2025-12-06 11:04:19 +01:00
{% extends "shared/frontend/base.html" %}
{% block title %}Leverandør Detaljer - BMC Hub{% endblock %}
{% block extra_css %}
2026-10-08 08:58:02 +02:00
< link rel = "stylesheet" href = "/static/css/company-contacts.css?v=20261005-2" >
2025-12-06 11:04:19 +01:00
< 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 >
2026-04-15 09:34:26 +02:00
< a class = "nav-link" href = "#kunder" data-tab = "kunder" >
< i class = "bi bi-building me-2" > < / i > Kunder
< / a >
2026-08-30 14:34:43 +02:00
< a class = "nav-link d-none" href = "#internetforbindelser" data-tab = "internetforbindelser" id = "internetConnectionsNav" >
< i class = "bi bi-router me-2" > < / i > Internetforbindelser
< / a >
2026-10-08 08:58:02 +02:00
< a class = "nav-link" href = "#filer" data-tab = "filer" > < i class = "bi bi-folder2-open me-2" > < / i > Filer< / a >
< a class = "nav-link" href = "#email" data-tab = "email" > < i class = "bi bi-envelope me-2" > < / i > E-mail< / a >
< a class = "nav-link" href = "#sager" data-tab = "sager" > < i class = "bi bi-journal-text me-2" > < / i > Sager< / a >
< a class = "nav-link" href = "#kontakter" data-tab = "kontakter" > < i class = "bi bi-people me-2" > < / i > Kontaktpersoner< / a >
2025-12-06 11:04:19 +01:00
< 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" >
2026-10-08 08:58:02 +02:00
< div id = "vendorResourcesStatus" class = "small mb-3" role = "status" aria-live = "polite" > < / div >
2025-12-06 11:04:19 +01:00
< 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 >
2026-07-30 20:11:43 +02:00
< 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 >
2025-12-06 11:04:19 +01:00
< / 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" >
2025-12-08 09:15:52 +01:00
< div class = "d-flex justify-content-between align-items-center mb-4" >
2026-08-17 19:27:43 +02:00
< h5 class = "mb-0 fw-bold" > Leverandørfakturaer< / h5 >
2025-12-08 09:15:52 +01:00
< 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 >
2025-12-06 11:04:19 +01:00
< / div >
< / div >
2026-04-15 09:34:26 +02:00
<!-- 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 >
2026-08-30 14:34:43 +02:00
< 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 >
2026-10-08 08:58:02 +02:00
< div class = "tab-pane fade" id = "filer" > < div class = "card p-4" >
< h5 > Filer< / h5 > < p class = "small text-muted" > Uploadede dokumenter og vedhæftninger fra leverandørens mails. Maks. 20 MB pr. upload.< / p >
< form id = "vendorFileUpload" class = "d-flex flex-wrap gap-2 mb-3" > < input class = "form-control" style = "max-width:420px" type = "file" name = "file" required aria-label = "Vælg fil" > < button class = "btn btn-primary" > Upload< / button > < / form > < div id = "vendorFilesList" class = "list-group" > Indlæser…< / div > < / div > < / div >
< div class = "tab-pane fade" id = "email" > < div class = "card p-4" > < h5 > E-mail< / h5 > < p class = "small text-muted" > Indgående og udgående mails koblet til leverandøren. Visning ændrer ikke læsestatus. Tråde grupperes på denne side.< / p >
< div class = "row g-3" > < div class = "col-xl-6" > < div id = "vendorEmailsList" > Indlæser…< / div > < div class = "d-flex gap-2 align-items-center" > < button id = "vendorMailPrevious" class = "btn btn-sm btn-outline-secondary" > Forrige< / button > < span id = "vendorMailPage" > < / span > < button id = "vendorMailNext" class = "btn btn-sm btn-outline-secondary" > Næste< / button > < / div > < / div > < div class = "col-xl-6" > < div id = "vendorMailPreview" class = "border rounded p-3" > Vælg en mail for at læse den.< / div > < / div > < / div > < / div > < / div >
< div class = "tab-pane fade" id = "sager" > < div class = "card p-4" > < h5 > Sager< / h5 > < p class = "small text-muted" > Direkte tilknyttede sager og sager knyttet via leverandørens mails.< / p > < label for = "vendorCaseSearch" class = "form-label" > Knyt eksisterende sag< / label > < input id = "vendorCaseSearch" class = "form-control mb-2" placeholder = "Søg på sagsnummer eller titel" > < div id = "vendorCaseResults" class = "list-group mb-3" > < / div > < div id = "vendorCasesList" class = "list-group" > Indlæser…< / div > < / div > < / div >
< div class = "tab-pane fade" id = "kontakter" >
{% with contact_owner_label='leverandør', contact_add_handler='showAddVendorContact' %}
{% include "shared/frontend/company_contacts_panel.html" %}
{% endwith %}
< details class = "mt-3" > < summary > Tilknyt eksisterende kontaktperson< / summary > < div class = "row g-2 my-2" > < div class = "col-md-8" > < label class = "form-label" for = "vendorContactSearch" > Kontaktperson< / label > < input id = "vendorContactSearch" class = "form-control" placeholder = "Søg navn eller e-mail" > < / div > < div class = "col-md-4" > < label for = "vendorContactRole" class = "form-label" > Rolle hos leverandøren< / label > < input id = "vendorContactRole" class = "form-control" maxlength = "100" placeholder = "Fx salg eller support" > < / div > < / div > < label class = "form-check mb-2" > < input id = "vendorContactPrimary" class = "form-check-input" type = "checkbox" > < span class = "form-check-label" > Primær kontaktperson< / span > < / label > < div id = "vendorContactResults" class = "list-group mb-3" > < / div > < div id = "vendorContactUnlinkList" class = "list-group mt-3" > < / div > < / details >
< / div >
2025-12-06 11:04:19 +01:00
<!-- 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 >
2025-12-08 09:15:52 +01:00
<!-- 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 >
2026-08-30 14:34:43 +02:00
< 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 >
2025-12-08 09:15:52 +01:00
< / 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 >
2025-12-06 11:04:19 +01:00
{% endblock %}
{% block extra_js %}
2026-10-08 08:58:02 +02:00
< script src = "/static/js/contact-mail-import.js?v=20261005-1" > < / script >
2025-12-06 11:04:19 +01:00
< 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);
2026-07-30 20:11:43 +02:00
await loadVendorEmailDomains();
2026-04-15 09:34:26 +02:00
await loadVendorCustomers();
2025-12-06 11:04:19 +01:00
} catch (error) {
console.error('Error loading vendor:', error);
document.getElementById('vendorName').textContent = 'Fejl ved indlæsning';
}
}
2026-07-30 20:11:43 +02:00
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();
}
2026-04-15 09:34:26 +02:00
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');
}
}
2025-12-06 11:04:19 +01:00
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}`;
2026-08-30 14:34:43 +02:00
document.getElementById('internetConnectionsNav').classList.toggle('d-none', !vendor.is_internet_provider);
if (vendor.is_internet_provider) loadVendorInternetConnections();
2025-12-06 11:04:19 +01:00
// 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 >
`;
2025-12-08 09:15:52 +01:00
// Load invoices
loadVendorInvoices();
}
async function loadVendorInvoices() {
try {
2026-07-28 14:18:24 +02:00
const response = await fetch(`/api/v1/vendors/${vendorId}/invoices`);
2025-12-08 09:15:52 +01:00
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 >
`;
}
}
2026-08-30 14:34:43 +02:00
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 > ';
}
}
2025-12-08 09:15:52 +01:00
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" >
2026-07-28 14:18:24 +02:00
< 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">
2025-12-08 09:15:52 +01:00
< 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',
2026-07-28 14:18:24 +02:00
'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'
2025-12-08 09:15:52 +01:00
};
return classes[status] || 'bg-secondary';
}
function getInvoiceStatusText(status) {
const texts = {
'unpaid': 'Ubetalt',
'paid': 'Betalt',
'overdue': 'Forfalden',
'cancelled': 'Annulleret',
2026-07-28 14:18:24 +02:00
'credited': 'Krediteret',
'rejected': 'Afvist',
'pending': 'Afventer',
'success': 'Behandlet',
'warning': 'Kræver kontrol',
'error': 'Fejl',
'extracted': 'Udtrukket',
'ai_extracted': 'Udtrukket',
'processed': 'Behandlet'
2025-12-08 09:15:52 +01:00
};
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);
2025-12-06 11:04:19 +01:00
}
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();
}
2025-12-08 09:15:52 +01:00
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;
2026-08-30 14:34:43 +02:00
document.getElementById('editIsInternetProvider').checked = Boolean(vendor.is_internet_provider);
2025-12-08 09:15:52 +01:00
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,
2026-08-30 14:34:43 +02:00
is_active: document.getElementById('editIsActive').checked,
is_internet_provider: document.getElementById('editIsInternetProvider').checked
2025-12-08 09:15:52 +01:00
};
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);
}
}
2025-12-06 11:04:19 +01:00
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');
});
});
2026-10-08 08:58:02 +02:00
// Open bookmarked supplier tab without changing existing navigation.
const initialTab = document.querySelector(`.vertical-nav [data-tab="${location.hash.slice(1).replace(/[^a-z]/g, '')}"]`);
if (initialTab) initialTab.click();
2025-12-06 11:04:19 +01:00
// Load vendor on page ready
document.addEventListener('DOMContentLoaded', loadVendor);
< / script >
2026-10-08 08:58:02 +02:00
< script src = "/static/js/company-contacts.js?v=20261005-2" > < / script >
< script src = "/static/js/vendor-resources.js?v=20261005-2" > < / script >
2025-12-06 11:04:19 +01:00
{% endblock %}