bmc_hub/app/vendors/frontend/vendor_detail.html
Christian adc4fb5876 Implement tests for sag module, add knowledge base templates, and enhance internet connection migrations
- Added multiple test cases for the sag module to ensure proper functionality and data handling.
- Created new templates for knowledge detail and knowledge index pages to display articles and solutions.
- Introduced migrations to enhance the internet connections schema, including new columns for manual sharing and SLA subscriptions.
- Added a script to reconcile known internet connections with verified data.
- Planned the implementation of a new website content administration module for managing customer references and operational status.
2026-08-30 14:34:43 +02:00

975 lines
41 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "shared/frontend/base.html" %}
{% block title %}Leverandør Detaljer - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.vendor-header {
background: linear-gradient(135deg, var(--accent) 0%, #1e6ba8 100%);
padding: 2rem;
border-radius: var(--border-radius);
color: white;
margin-bottom: 2rem;
}
.vendor-avatar-large {
width: 80px;
height: 80px;
border-radius: 16px;
background: rgba(255,255,255,0.2);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 2rem;
border: 3px solid rgba(255,255,255,0.3);
}
.vertical-nav {
position: sticky;
top: 100px;
}
.vertical-nav .nav-link {
color: var(--text-secondary);
padding: 0.75rem 1rem;
border-radius: 8px;
margin-bottom: 0.5rem;
transition: all 0.2s;
border-left: 3px solid transparent;
}
.vertical-nav .nav-link:hover,
.vertical-nav .nav-link.active {
background: var(--accent-light);
color: var(--accent);
border-left-color: var(--accent);
}
.info-row {
padding: 1rem 0;
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: 500;
margin-bottom: 0.25rem;
}
.info-value {
color: var(--text-primary);
font-size: 1rem;
}
.category-badge-large {
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 1rem;
font-weight: 500;
display: inline-block;
}
.priority-indicator {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.25rem;
}
</style>
{% endblock %}
{% block content %}
<!-- Vendor Header -->
<div class="vendor-header" id="vendorHeader">
<div class="container-fluid">
<div class="row align-items-center">
<div class="col-auto">
<div class="vendor-avatar-large" id="vendorAvatar"></div>
</div>
<div class="col">
<div class="d-flex align-items-center gap-3 mb-2">
<h2 class="mb-0 fw-bold" id="vendorName">Loading...</h2>
<span class="badge bg-white text-dark" id="vendorStatus"></span>
</div>
<div class="d-flex gap-4 text-white-50">
<span id="vendorDomain"></span>
<span id="vendorCategory"></span>
</div>
</div>
<div class="col-auto">
<button class="btn btn-light" onclick="editVendor()">
<i class="bi bi-pencil me-2"></i>Rediger
</button>
</div>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row">
<!-- Vertical Navigation -->
<div class="col-lg-2">
<div class="vertical-nav">
<nav class="nav flex-column">
<a class="nav-link active" href="#oversigt" data-tab="oversigt">
<i class="bi bi-info-circle me-2"></i>Oversigt
</a>
<a class="nav-link" href="#produkter" data-tab="produkter">
<i class="bi bi-box-seam me-2"></i>Produkter
</a>
<a class="nav-link" href="#fakturaer" data-tab="fakturaer">
<i class="bi bi-receipt me-2"></i>Fakturaer
</a>
<a class="nav-link" href="#kunder" data-tab="kunder">
<i class="bi bi-building me-2"></i>Kunder
</a>
<a class="nav-link d-none" href="#internetforbindelser" data-tab="internetforbindelser" id="internetConnectionsNav">
<i class="bi bi-router me-2"></i>Internetforbindelser
</a>
<a class="nav-link" href="#aktivitet" data-tab="aktivitet">
<i class="bi bi-clock-history me-2"></i>Aktivitet
</a>
</nav>
</div>
</div>
<!-- Content Area -->
<div class="col-lg-10">
<div class="tab-content">
<!-- Oversigt Tab -->
<div class="tab-pane fade show active" id="oversigt">
<div class="row g-4">
<!-- Vendor Information -->
<div class="col-lg-6">
<div class="card p-4">
<h5 class="mb-4 fw-bold">Leverandør Information</h5>
<div id="vendorInfo">
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
</div>
<!-- Contact & System Info -->
<div class="col-lg-6">
<div class="card p-4 mb-4">
<h5 class="mb-4 fw-bold">Kontakt Information</h5>
<div id="contactInfo">
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
<div class="card p-4">
<h5 class="mb-4 fw-bold">System Information</h5>
<div id="systemInfo">
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
<div class="card p-4 mt-4">
<h5 class="mb-2 fw-bold">Emaildomæner</h5>
<p class="small text-muted">Kun eksakte domæner bruges til automatisk match.</p>
<div class="input-group input-group-sm mb-3">
<input id="vendorEmailDomainInput" class="form-control" placeholder="fx cloudfactorygroup.com">
<button class="btn btn-primary" onclick="addVendorEmailDomain()">Tilføj</button>
</div>
<div id="vendorEmailDomains" class="d-flex flex-wrap gap-2"></div>
</div>
</div>
</div>
</div>
<!-- Produkter Tab -->
<div class="tab-pane fade" id="produkter">
<div class="card p-4">
<h5 class="mb-4 fw-bold">Produkter fra denne leverandør</h5>
<p class="text-muted">Produkt tracking kommer snart...</p>
</div>
</div>
<!-- Fakturaer Tab -->
<div class="tab-pane fade" id="fakturaer">
<div class="card p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="mb-0 fw-bold">Leverandørfakturaer</h5>
<span class="badge bg-primary" id="invoiceCount">0</span>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Fakturanr.</th>
<th>Dato</th>
<th>Forfald</th>
<th>Beløb</th>
<th>Status</th>
<th class="text-end">Handling</th>
</tr>
</thead>
<tbody id="invoicesTableBody">
<tr>
<td colspan="6" class="text-center py-4">
<div class="spinner-border text-primary" role="status"></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Kunder Tab -->
<div class="tab-pane fade" id="kunder">
<div class="card p-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0 fw-bold">Linked kunder</h5>
<span class="badge bg-primary" id="vendorCustomersCount">0</span>
</div>
<div class="row g-2 mb-3">
<div class="col-md-8">
<input
type="text"
class="form-control"
id="vendorCustomerSearch"
placeholder="Søg kunde (navn, email, CVR)"
oninput="searchCustomersForVendor(this.value)"
>
</div>
<div class="col-md-4 text-md-end">
<small class="text-muted">Link leverandør til kunde</small>
</div>
</div>
<div id="vendorCustomerSearchResults" class="list-group mb-3" style="display:none;"></div>
<div id="vendorCustomersList" class="list-group mb-2"></div>
<div id="vendorCustomersEmpty" class="text-muted">Ingen linked kunder endnu.</div>
</div>
</div>
<div class="tab-pane fade" id="internetforbindelser">
<div class="card p-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0 fw-bold">Internetforbindelser</h5>
<span class="badge bg-primary" id="internetConnectionsCount">0</span>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead><tr><th>Forbindelse</th><th>Kredsløb</th><th>Adresse</th><th>Kunde</th><th>Hastighed</th><th>Status</th><th></th></tr></thead>
<tbody id="internetConnectionsBody"><tr><td colspan="7" class="text-center text-muted py-4">Indlæser…</td></tr></tbody>
</table>
</div>
</div>
</div>
<!-- Aktivitet Tab -->
<div class="tab-pane fade" id="aktivitet">
<div class="card p-4">
<h5 class="mb-4 fw-bold">Aktivitetslog</h5>
<p class="text-muted">Aktivitetshistorik kommer snart...</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Vendor Modal -->
<div class="modal fade" id="editVendorModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-pencil me-2"></i>Rediger Leverandør</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editVendorForm">
<div class="row mb-3">
<div class="col-md-8">
<label class="form-label">Navn *</label>
<input type="text" class="form-control" id="editName" required>
</div>
<div class="col-md-4">
<label class="form-label">CVR-nummer</label>
<input type="text" class="form-control" id="editCvr" maxlength="8" pattern="[0-9]{8}">
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Kategori</label>
<select class="form-select" id="editCategory">
<option value="IT & Software">IT & Software</option>
<option value="Telefoni & Internet">Telefoni & Internet</option>
<option value="Hardware">Hardware</option>
<option value="Cloud Services">Cloud Services</option>
<option value="Hosting">Hosting</option>
<option value="Security">Security</option>
<option value="Consulting">Consulting</option>
<option value="Andet">Andet</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Domæne</label>
<input type="text" class="form-control" id="editDomain" placeholder="example.com">
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Email</label>
<input type="email" class="form-control" id="editEmail">
</div>
<div class="col-md-6">
<label class="form-label">Telefon</label>
<input type="tel" class="form-control" id="editPhone">
</div>
</div>
<div class="row mb-3">
<div class="col-md-8">
<label class="form-label">Adresse</label>
<input type="text" class="form-control" id="editAddress">
</div>
<div class="col-md-4">
<label class="form-label">Postnr. & By</label>
<input type="text" class="form-control" id="editCity">
</div>
</div>
<div class="mb-3">
<label class="form-label">e-conomic Leverandør Nr.</label>
<input type="text" class="form-control" id="editEconomicNumber">
<div class="form-text">Til integration med e-conomic</div>
</div>
<div class="mb-3">
<label class="form-label">Noter</label>
<textarea class="form-control" id="editNotes" rows="3"></textarea>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="editIsActive">
<label class="form-check-label" for="editIsActive">Aktiv leverandør</label>
</div>
<div class="form-check form-switch mt-3">
<input class="form-check-input" type="checkbox" id="editIsInternetProvider">
<label class="form-check-label" for="editIsInternetProvider">Internetleverandør</label>
<div class="form-text">Kan vælges som leverandør på internetforbindelser.</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
<button type="button" class="btn btn-primary" onclick="saveVendor()">
<i class="bi bi-save me-2"></i>Gem Ændringer
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
const vendorId = {{ vendor_id }};
async function loadVendor() {
try {
const response = await fetch(`/api/v1/vendors/${vendorId}`);
if (!response.ok) {
throw new Error('Vendor not found');
}
const vendor = await response.json();
displayVendor(vendor);
await loadVendorEmailDomains();
await loadVendorCustomers();
} catch (error) {
console.error('Error loading vendor:', error);
document.getElementById('vendorName').textContent = 'Fejl ved indlæsning';
}
}
async function loadVendorEmailDomains() {
const host = document.getElementById('vendorEmailDomains');
if (!host) return;
try {
const response = await fetch(`/api/v1/vendors/${vendorId}/email-domains`);
if (!response.ok) throw new Error('Kunne ikke hente domæner');
const rows = await response.json();
host.innerHTML = rows.length ? rows.map(row => `
<span class="badge bg-light text-dark border p-2">
${escapeHtml(row.domain)}
<button class="btn btn-link btn-sm text-danger p-0 ms-2" onclick="deleteVendorEmailDomain('${encodeURIComponent(row.domain)}')" title="Fjern">×</button>
</span>
`).join('') : '<span class="small text-muted">Ingen domæner registreret.</span>';
} catch (error) {
host.innerHTML = `<span class="small text-danger">${escapeHtml(error.message)}</span>`;
}
}
async function addVendorEmailDomain() {
const input = document.getElementById('vendorEmailDomainInput');
const domain = input.value.trim();
if (!domain) return;
const response = await fetch(`/api/v1/vendors/${vendorId}/email-domains`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ domain })
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
alert(error.detail || 'Domænet kunne ikke tilføjes');
return;
}
input.value = '';
await loadVendorEmailDomains();
}
async function deleteVendorEmailDomain(encodedDomain) {
const response = await fetch(`/api/v1/vendors/${vendorId}/email-domains/${encodedDomain}`, { method: 'DELETE' });
if (!response.ok) {
const error = await response.json().catch(() => ({}));
alert(error.detail || 'Domænet kunne ikke fjernes');
return;
}
await loadVendorEmailDomains();
}
async function loadVendorCustomers() {
const listEl = document.getElementById('vendorCustomersList');
const emptyEl = document.getElementById('vendorCustomersEmpty');
const countEl = document.getElementById('vendorCustomersCount');
if (!listEl || !emptyEl || !countEl) return;
try {
const response = await fetch(`/api/v1/vendors/${vendorId}/customers`);
if (!response.ok) throw new Error('Kunne ikke hente linked kunder');
const links = await response.json();
const rows = Array.isArray(links) ? links : [];
countEl.textContent = String(rows.length);
if (!rows.length) {
listEl.innerHTML = '';
emptyEl.classList.remove('d-none');
return;
}
emptyEl.classList.add('d-none');
listEl.innerHTML = rows.map((row) => `
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<div class="fw-semibold">${escapeHtml(row.customer_name || `Kunde #${row.customer_id}`)}</div>
<div class="small text-muted">${row.customer_cvr ? `CVR ${escapeHtml(row.customer_cvr)} · ` : ''}${row.customer_email ? escapeHtml(row.customer_email) : '-'}</div>
</div>
<div class="d-flex align-items-center gap-2">
<span class="badge bg-light text-dark border">${escapeHtml(row.relationship_type || 'supplier')}</span>
<a class="btn btn-sm btn-outline-primary" href="/customers/${row.customer_id}">
<i class="bi bi-box-arrow-up-right"></i>
</a>
<button class="btn btn-sm btn-outline-danger" onclick="unlinkCustomerFromVendor(${row.customer_id})">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading vendor customers:', error);
listEl.innerHTML = '';
emptyEl.classList.remove('d-none');
}
}
let customerSearchDebounce = null;
async function searchCustomersForVendor(query) {
const resultsEl = document.getElementById('vendorCustomerSearchResults');
if (!resultsEl) return;
const q = String(query || '').trim();
if (!q) {
resultsEl.style.display = 'none';
resultsEl.innerHTML = '';
return;
}
if (customerSearchDebounce) window.clearTimeout(customerSearchDebounce);
customerSearchDebounce = window.setTimeout(async () => {
try {
const response = await fetch(`/api/v1/customers?search=${encodeURIComponent(q)}&limit=10&offset=0`);
if (!response.ok) throw new Error('Søgning fejlede');
const payload = await response.json();
const customers = Array.isArray(payload?.customers) ? payload.customers : [];
if (!customers.length) {
resultsEl.innerHTML = '<div class="list-group-item text-muted">Ingen kunder fundet</div>';
resultsEl.style.display = 'block';
return;
}
resultsEl.innerHTML = customers.map((c) => `
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<div class="fw-semibold">${escapeHtml(c.name || '')}</div>
<div class="small text-muted">${c.cvr_number ? `CVR ${escapeHtml(c.cvr_number)} · ` : ''}${c.email ? escapeHtml(c.email) : '-'}</div>
</div>
<button class="btn btn-sm btn-primary" onclick="linkCustomerToVendorFromUI(${c.id})">
<i class="bi bi-link-45deg me-1"></i>Link
</button>
</div>
`).join('');
resultsEl.style.display = 'block';
} catch (error) {
console.error('Customer search failed:', error);
resultsEl.innerHTML = '<div class="list-group-item text-danger">Søgning fejlede</div>';
resultsEl.style.display = 'block';
}
}, 220);
}
async function linkCustomerToVendorFromUI(customerId) {
try {
const response = await fetch(`/api/v1/vendors/${vendorId}/customers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customer_id: customerId, relationship_type: 'supplier' })
});
if (!response.ok) {
const payload = await response.json().catch(() => ({}));
throw new Error(payload.detail || 'Kunne ikke linke kunde');
}
const input = document.getElementById('vendorCustomerSearch');
const results = document.getElementById('vendorCustomerSearchResults');
if (input) input.value = '';
if (results) {
results.innerHTML = '';
results.style.display = 'none';
}
await loadVendorCustomers();
} catch (error) {
alert(error.message || 'Kunne ikke linke kunde');
}
}
async function unlinkCustomerFromVendor(customerId) {
if (!confirm('Fjern link mellem leverandør og kunde?')) return;
try {
const response = await fetch(`/api/v1/vendors/${vendorId}/customers/${customerId}`, {
method: 'DELETE'
});
if (!response.ok) {
const payload = await response.json().catch(() => ({}));
throw new Error(payload.detail || 'Kunne ikke fjerne link');
}
await loadVendorCustomers();
} catch (error) {
alert(error.message || 'Kunne ikke fjerne link');
}
}
function displayVendor(vendor) {
// Header
document.getElementById('vendorName').textContent = vendor.name;
document.getElementById('vendorAvatar').textContent = getInitials(vendor.name);
document.getElementById('vendorStatus').textContent = vendor.is_active ? 'Aktiv' : 'Inaktiv';
document.getElementById('vendorStatus').className = `badge ${vendor.is_active ? 'bg-success' : 'bg-secondary'}`;
document.getElementById('vendorDomain').innerHTML = vendor.domain ? `<i class="bi bi-globe me-2"></i>${vendor.domain}` : '';
document.getElementById('vendorCategory').innerHTML = `${getCategoryIcon(vendor.category)} ${vendor.category}`;
document.getElementById('internetConnectionsNav').classList.toggle('d-none', !vendor.is_internet_provider);
if (vendor.is_internet_provider) loadVendorInternetConnections();
// Update page title
document.title = `${vendor.name} - BMC Hub`;
// Vendor Info
document.getElementById('vendorInfo').innerHTML = `
${vendor.cvr_number ? `
<div class="info-row">
<div class="info-label">CVR-nummer</div>
<div class="info-value fw-semibold">${escapeHtml(vendor.cvr_number)}</div>
</div>
` : ''}
<div class="info-row">
<div class="info-label">Kategori</div>
<div class="info-value">
<span class="category-badge-large bg-light">
${getCategoryIcon(vendor.category)} ${escapeHtml(vendor.category)}
</span>
</div>
</div>
${vendor.economic_supplier_number ? `
<div class="info-row">
<div class="info-label">e-conomic Leverandør Nr.</div>
<div class="info-value">${vendor.economic_supplier_number}</div>
</div>
` : ''}
${vendor.notes ? `
<div class="info-row">
<div class="info-label">Noter</div>
<div class="info-value">${escapeHtml(vendor.notes)}</div>
</div>
` : ''}
`;
// Contact Info
document.getElementById('contactInfo').innerHTML = `
${vendor.email ? `
<div class="info-row">
<div class="info-label">Email</div>
<div class="info-value">
<a href="mailto:${escapeHtml(vendor.email)}" class="text-decoration-none">
<i class="bi bi-envelope me-2"></i>${escapeHtml(vendor.email)}
</a>
</div>
</div>
` : ''}
${vendor.phone ? `
<div class="info-row">
<div class="info-label">Telefon</div>
<div class="info-value">
<a href="tel:${escapeHtml(vendor.phone)}" class="text-decoration-none">
<i class="bi bi-telephone me-2"></i>${escapeHtml(vendor.phone)}
</a>
</div>
</div>
` : ''}
${vendor.website ? `
<div class="info-row">
<div class="info-label">Website</div>
<div class="info-value">
<a href="${escapeHtml(vendor.website)}" target="_blank" class="text-decoration-none">
<i class="bi bi-globe me-2"></i>${escapeHtml(vendor.website)}
</a>
</div>
</div>
` : ''}
${vendor.address ? `
<div class="info-row">
<div class="info-label">Adresse</div>
<div class="info-value">
<i class="bi bi-geo-alt me-2"></i>
${escapeHtml(vendor.address)}
${vendor.postal_code || vendor.city ? `<br>${vendor.postal_code ? escapeHtml(vendor.postal_code) + ' ' : ''}${vendor.city ? escapeHtml(vendor.city) : ''}` : ''}
${vendor.country && vendor.country !== 'Danmark' ? `<br>${escapeHtml(vendor.country)}` : ''}
</div>
</div>
` : ''}
${vendor.email_pattern ? `
<div class="info-row">
<div class="info-label">Email Pattern</div>
<div class="info-value"><code>${escapeHtml(vendor.email_pattern)}</code></div>
</div>
` : ''}
`;
// System Info
document.getElementById('systemInfo').innerHTML = `
<div class="info-row">
<div class="info-label">Oprettet</div>
<div class="info-value">${formatDate(vendor.created_at)}</div>
</div>
${vendor.updated_at ? `
<div class="info-row">
<div class="info-label">Sidst opdateret</div>
<div class="info-value">${formatDate(vendor.updated_at)}</div>
</div>
` : ''}
<div class="info-row">
<div class="info-label">ID</div>
<div class="info-value"><code>#${vendor.id}</code></div>
</div>
`;
// Load invoices
loadVendorInvoices();
}
async function loadVendorInvoices() {
try {
const response = await fetch(`/api/v1/vendors/${vendorId}/invoices`);
if (!response.ok) throw new Error('Failed to load invoices');
const invoices = await response.json();
displayInvoices(invoices);
} catch (error) {
console.error('Error loading invoices:', error);
document.getElementById('invoicesTableBody').innerHTML = `
<tr>
<td colspan="6" class="text-center text-muted py-4">
Kunne ikke indlæse fakturaer
</td>
</tr>
`;
}
}
async function loadVendorInternetConnections() {
const body = document.getElementById('internetConnectionsBody');
try {
const response = await fetch(`/api/v1/vendors/${vendorId}/internet-connections`);
if (!response.ok) throw new Error('Kunne ikke hente forbindelser');
const rows = await response.json();
document.getElementById('internetConnectionsCount').textContent = rows.length;
body.innerHTML = rows.length ? rows.map((row) => `
<tr>
<td class="fw-semibold">${escapeHtml(row.name || '-')}</td>
<td>${escapeHtml(row.circuit_number || '-')}</td>
<td>${escapeHtml(row.address || '-')}</td>
<td>${escapeHtml(row.customer_name || 'Ikke allokeret')}</td>
<td>${row.download_mbps || row.upload_mbps ? `${Number(row.download_mbps || 0)}/${Number(row.upload_mbps || 0)} Mbps` : '-'}</td>
<td><span class="badge bg-light text-dark border">${escapeHtml(row.status || '-')}</span></td>
<td class="text-end"><a class="btn btn-sm btn-outline-primary" href="/economy/internet-connections/${row.id}"><i class="bi bi-box-arrow-up-right"></i></a></td>
</tr>`).join('') : '<tr><td colspan="7" class="text-center text-muted py-4">Ingen forbindelser koblet til leverandøren.</td></tr>';
} catch (error) {
body.innerHTML = '<tr><td colspan="7" class="text-center text-danger py-4">Kunne ikke hente forbindelser.</td></tr>';
}
}
function displayInvoices(invoices) {
const tbody = document.getElementById('invoicesTableBody');
const count = document.getElementById('invoiceCount');
count.textContent = invoices.length;
if (invoices.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="text-center text-muted py-4">
Ingen fakturaer fundet for denne leverandør
</td>
</tr>
`;
return;
}
tbody.innerHTML = invoices.map(invoice => {
const statusClass = getInvoiceStatusClass(invoice.status);
const statusText = getInvoiceStatusText(invoice.status);
return `
<tr>
<td><strong>${escapeHtml(invoice.invoice_number)}</strong></td>
<td>${formatDateShort(invoice.invoice_date)}</td>
<td>${formatDateShort(invoice.due_date)}</td>
<td><strong>${formatCurrency(invoice.total_amount, invoice.currency)}</strong></td>
<td><span class="badge ${statusClass}">${statusText}</span></td>
<td class="text-end">
<a href="${invoice.source_type === 'supplier_invoice'
? `/billing/supplier-invoices?invoice=${invoice.id}`
: '/economy/internet-connections#invoice-sync'}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
`;
}).join('');
}
function getInvoiceStatusClass(status) {
const classes = {
'unpaid': 'bg-warning text-dark',
'paid': 'bg-success',
'overdue': 'bg-danger',
'cancelled': 'bg-secondary',
'credited': 'bg-secondary',
'rejected': 'bg-secondary',
'pending': 'bg-info',
'success': 'bg-success',
'warning': 'bg-warning text-dark',
'error': 'bg-danger',
'extracted': 'bg-info text-dark',
'ai_extracted': 'bg-info text-dark',
'processed': 'bg-success'
};
return classes[status] || 'bg-secondary';
}
function getInvoiceStatusText(status) {
const texts = {
'unpaid': 'Ubetalt',
'paid': 'Betalt',
'overdue': 'Forfalden',
'cancelled': 'Annulleret',
'credited': 'Krediteret',
'rejected': 'Afvist',
'pending': 'Afventer',
'success': 'Behandlet',
'warning': 'Kræver kontrol',
'error': 'Fejl',
'extracted': 'Udtrukket',
'ai_extracted': 'Udtrukket',
'processed': 'Behandlet'
};
return texts[status] || status;
}
function formatDateShort(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
return date.toLocaleDateString('da-DK', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
function formatCurrency(amount, currency = 'DKK') {
if (!amount) return '-';
return new Intl.NumberFormat('da-DK', {
style: 'currency',
currency: currency
}).format(amount);
}
function getCategoryIcon(category) {
const icons = {
hardware: '🖥️',
software: '💻',
telecom: '📡',
services: '🛠️',
hosting: '☁️',
general: '📦'
};
return icons[category] || '📦';
}
function getPriorityClass(priority) {
if (priority >= 80) return 'bg-danger text-white';
if (priority >= 60) return 'bg-warning';
if (priority >= 40) return 'bg-info';
return 'bg-secondary text-white';
}
function getInitials(name) {
return name.split(' ').map(word => word[0]).join('').substring(0, 2).toUpperCase();
}
function editVendor() {
// Get current vendor data and populate form
fetch(`/api/v1/vendors/${vendorId}`)
.then(response => response.json())
.then(vendor => {
document.getElementById('editName').value = vendor.name || '';
document.getElementById('editCvr').value = vendor.cvr_number || '';
document.getElementById('editCategory').value = vendor.category || 'Andet';
document.getElementById('editDomain').value = vendor.domain || '';
document.getElementById('editEmail').value = vendor.email || '';
document.getElementById('editPhone').value = vendor.phone || '';
document.getElementById('editAddress').value = vendor.address || '';
document.getElementById('editCity').value = vendor.city || '';
document.getElementById('editEconomicNumber').value = vendor.economic_supplier_number || '';
document.getElementById('editNotes').value = vendor.notes || '';
document.getElementById('editIsActive').checked = vendor.is_active;
document.getElementById('editIsInternetProvider').checked = Boolean(vendor.is_internet_provider);
new bootstrap.Modal(document.getElementById('editVendorModal')).show();
})
.catch(error => {
console.error('Error loading vendor for edit:', error);
alert('Kunne ikke hente leverandør data');
});
}
async function saveVendor() {
try {
const data = {
name: document.getElementById('editName').value.trim(),
cvr_number: document.getElementById('editCvr').value.trim() || null,
category: document.getElementById('editCategory').value,
domain: document.getElementById('editDomain').value.trim() || null,
email: document.getElementById('editEmail').value.trim() || null,
phone: document.getElementById('editPhone').value.trim() || null,
address: document.getElementById('editAddress').value.trim() || null,
city: document.getElementById('editCity').value.trim() || null,
economic_supplier_number: document.getElementById('editEconomicNumber').value.trim() || null,
notes: document.getElementById('editNotes').value.trim() || null,
is_active: document.getElementById('editIsActive').checked,
is_internet_provider: document.getElementById('editIsInternetProvider').checked
};
if (!data.name) {
alert('Navn er påkrævet');
return;
}
const response = await fetch(`/api/v1/vendors/${vendorId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Kunne ikke gemme ændringer');
}
// Close modal and reload vendor
bootstrap.Modal.getInstance(document.getElementById('editVendorModal')).hide();
await loadVendor();
alert('✅ Leverandør opdateret!');
} catch (error) {
console.error('Error saving vendor:', error);
alert('Fejl: ' + error.message);
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('da-DK', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
// Tab navigation
document.querySelectorAll('.vertical-nav .nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const tab = link.dataset.tab;
// Update nav
document.querySelectorAll('.vertical-nav .nav-link').forEach(l => l.classList.remove('active'));
link.classList.add('active');
// Update content
document.querySelectorAll('.tab-pane').forEach(pane => {
pane.classList.remove('show', 'active');
});
document.getElementById(tab).classList.add('show', 'active');
});
});
// Load vendor on page ready
document.addEventListener('DOMContentLoaded', loadVendor);
</script>
{% endblock %}