2025-12-06 11:04:19 +01:00
|
|
|
{% 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="#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>
|
|
|
|
|
</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">
|
|
|
|
|
<h5 class="mb-0 fw-bold">Leverandør Fakturaer</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>
|
2025-12-06 11:04:19 +01:00
|
|
|
</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>
|
|
|
|
|
|
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>
|
|
|
|
|
</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 %}
|
|
|
|
|
<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);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading vendor:', error);
|
|
|
|
|
document.getElementById('vendorName').textContent = 'Fejl ved indlæsning';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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}`;
|
|
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
|
const response = await fetch(`/api/v1/supplier-invoices?vendor_id=${vendorId}`);
|
|
|
|
|
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>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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="/billing/supplier-invoices?invoice=${invoice.id}" 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',
|
|
|
|
|
'pending': 'bg-info'
|
|
|
|
|
};
|
|
|
|
|
return classes[status] || 'bg-secondary';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getInvoiceStatusText(status) {
|
|
|
|
|
const texts = {
|
|
|
|
|
'unpaid': 'Ubetalt',
|
|
|
|
|
'paid': 'Betalt',
|
|
|
|
|
'overdue': 'Forfalden',
|
|
|
|
|
'cancelled': 'Annulleret',
|
|
|
|
|
'pending': 'Afventer'
|
|
|
|
|
};
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Load vendor on page ready
|
|
|
|
|
document.addEventListener('DOMContentLoaded', loadVendor);
|
|
|
|
|
</script>
|
|
|
|
|
{% endblock %}
|