bmc_hub/app/vendors/frontend/vendor_detail.html
Christian 4871dd2d9e Add comprehensive test coverage for various modules
- Introduced tests for mail templates including ranking, rendering, and attachment handling.
- Added tests for time billing selection and approval processes, ensuring proper validation and state management.
- Implemented tests for case types in JavaScript, focusing on loading and editing functionality.
- Created tests for contact mail AI integration, ensuring proper parsing and error handling.
- Developed tests for dark theme CSS contrast and loading order.
- Added tests for invoice fee service, covering various scenarios including exemptions and invalid inputs.
- Implemented vendor resource tests, ensuring proper file handling and query validation.
2026-10-08 08:58:02 +02:00

1001 lines
45 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 %}
<link rel="stylesheet" href="/static/css/company-contacts.css?v=20261005-2">
<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="#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>
<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 id="vendorResourcesStatus" class="small mb-3" role="status" aria-live="polite"></div>
<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>
<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>
<!-- 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 src="/static/js/contact-mail-import.js?v=20261005-1"></script>
<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');
});
});
// 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();
// Load vendor on page ready
document.addEventListener('DOMContentLoaded', loadVendor);
</script>
<script src="/static/js/company-contacts.js?v=20261005-2"></script>
<script src="/static/js/vendor-resources.js?v=20261005-2"></script>
{% endblock %}