2025-12-06 11:04:19 +01:00
|
|
|
{% extends "shared/frontend/base.html" %}
|
|
|
|
|
|
|
|
|
|
{% block title %}Leverandører - BMC Hub{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block extra_css %}
|
|
|
|
|
<style>
|
|
|
|
|
.filter-btn {
|
|
|
|
|
background: var(--bg-card);
|
|
|
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
padding: 0.5rem 1.2rem;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-btn:hover, .filter-btn.active {
|
|
|
|
|
background: var(--accent);
|
|
|
|
|
color: white;
|
|
|
|
|
border-color: var(--accent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vendor-avatar {
|
|
|
|
|
width: 40px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
background: var(--accent-light);
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.category-badge {
|
|
|
|
|
padding: 0.25rem 0.75rem;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.priority-badge {
|
|
|
|
|
width: 30px;
|
|
|
|
|
height: 30px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
}
|
2026-08-30 14:34:43 +02:00
|
|
|
|
|
|
|
|
.vendor-form-section {
|
|
|
|
|
border: 1px solid var(--border-color, #e5e7eb);
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
padding: 1.15rem;
|
|
|
|
|
background: var(--bg-card, #fff);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vendor-form-section-title {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: .55rem;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vendor-type-switch {
|
|
|
|
|
border: 1px solid rgba(13, 110, 253, .2);
|
|
|
|
|
background: rgba(13, 110, 253, .05);
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 1rem 1rem 1rem 3rem;
|
|
|
|
|
}
|
2025-12-06 11:04:19 +01:00
|
|
|
</style>
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block content %}
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-5">
|
|
|
|
|
<div>
|
|
|
|
|
<h2 class="fw-bold mb-1">Leverandører</h2>
|
|
|
|
|
<p class="text-muted mb-0">Administrer dine leverandører og partnere</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="d-flex gap-3">
|
|
|
|
|
<input type="text" id="searchInput" class="header-search" placeholder="Søg leverandør, CVR, domain...">
|
|
|
|
|
<button class="btn btn-primary" onclick="showCreateVendorModal()">
|
|
|
|
|
<i class="bi bi-plus-lg me-2"></i>Opret Leverandør
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="mb-4 d-flex gap-2 flex-wrap">
|
|
|
|
|
<button class="filter-btn active" data-filter="all" onclick="setFilter('all')">
|
|
|
|
|
Alle <span id="countAll" class="ms-1"></span>
|
|
|
|
|
</button>
|
|
|
|
|
<button class="filter-btn" data-filter="hardware" onclick="setFilter('hardware')">
|
|
|
|
|
Hardware
|
|
|
|
|
</button>
|
|
|
|
|
<button class="filter-btn" data-filter="software" onclick="setFilter('software')">
|
|
|
|
|
Software
|
|
|
|
|
</button>
|
|
|
|
|
<button class="filter-btn" data-filter="telecom" onclick="setFilter('telecom')">
|
|
|
|
|
Telekom
|
|
|
|
|
</button>
|
|
|
|
|
<button class="filter-btn" data-filter="services" onclick="setFilter('services')">
|
|
|
|
|
Services
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card p-4">
|
|
|
|
|
<div class="table-responsive">
|
|
|
|
|
<table class="table table-hover align-middle">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>Leverandør</th>
|
|
|
|
|
<th>Kontakt Info</th>
|
|
|
|
|
<th>CVR</th>
|
|
|
|
|
<th>Kategori</th>
|
|
|
|
|
<th>Status</th>
|
|
|
|
|
<th class="text-end">Handlinger</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody id="vendorsTableBody">
|
|
|
|
|
<tr>
|
2025-12-08 09:15:52 +01:00
|
|
|
<td colspan="6" class="text-center py-5">
|
2025-12-06 11:04:19 +01:00
|
|
|
<div class="spinner-border text-primary" role="status">
|
|
|
|
|
<span class="visually-hidden">Loading...</span>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Pagination -->
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center mt-4">
|
|
|
|
|
<div class="text-muted small">
|
|
|
|
|
Viser <span id="showingStart">0</span>-<span id="showingEnd">0</span> af <span id="totalCount">0</span> leverandører
|
|
|
|
|
</div>
|
|
|
|
|
<div class="d-flex gap-2">
|
|
|
|
|
<button class="btn btn-sm btn-outline-secondary" id="prevBtn" onclick="previousPage()">
|
|
|
|
|
<i class="bi bi-chevron-left"></i> Forrige
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn btn-sm btn-outline-secondary" id="nextBtn" onclick="nextPage()">
|
|
|
|
|
Næste <i class="bi bi-chevron-right"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Create Vendor Modal -->
|
|
|
|
|
<div class="modal fade" id="createVendorModal" tabindex="-1">
|
2026-08-30 14:34:43 +02:00
|
|
|
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
2025-12-06 11:04:19 +01:00
|
|
|
<div class="modal-content">
|
|
|
|
|
<div class="modal-header">
|
2026-08-30 14:34:43 +02:00
|
|
|
<div>
|
|
|
|
|
<div class="small text-uppercase text-muted fw-semibold">Leverandørkartotek</div>
|
|
|
|
|
<h5 class="modal-title">Opret ny leverandør</h5>
|
|
|
|
|
</div>
|
2025-12-06 11:04:19 +01:00
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
2026-08-30 14:34:43 +02:00
|
|
|
<form id="createVendorForm" novalidate>
|
|
|
|
|
<div class="alert d-none" id="createVendorFeedback" role="alert"></div>
|
2025-12-06 11:04:19 +01:00
|
|
|
<div class="row g-3">
|
2026-08-30 14:34:43 +02:00
|
|
|
<div class="col-lg-7">
|
|
|
|
|
<div class="vendor-form-section h-100">
|
|
|
|
|
<div class="vendor-form-section-title"><i class="bi bi-building"></i>Virksomhed</div>
|
|
|
|
|
<div class="row g-3">
|
2025-12-06 11:04:19 +01:00
|
|
|
<div class="col-md-8">
|
2026-08-30 14:34:43 +02:00
|
|
|
<label class="form-label" for="name">Virksomhedsnavn <span class="text-danger">*</span></label>
|
|
|
|
|
<input type="text" class="form-control" id="name" required maxlength="255" autocomplete="organization" autofocus>
|
|
|
|
|
<div class="invalid-feedback">Angiv leverandørens navn.</div>
|
2025-12-06 11:04:19 +01:00
|
|
|
</div>
|
|
|
|
|
<div class="col-md-4">
|
2026-08-30 14:34:43 +02:00
|
|
|
<label class="form-label" for="cvr_number">CVR-nummer</label>
|
|
|
|
|
<div class="input-group">
|
|
|
|
|
<input type="text" inputmode="numeric" class="form-control" id="cvr_number" maxlength="8" pattern="[0-9]{8}" placeholder="12345678">
|
|
|
|
|
<button class="btn btn-outline-primary" type="button" id="vendorCvrLookupBtn" onclick="lookupVendorCvr()">Hent</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="invalid-feedback">CVR skal bestå af 8 cifre.</div>
|
|
|
|
|
<div class="form-text" id="vendorCvrLookupStatus">Indtast CVR og klik Hent for autofyld.</div>
|
2025-12-06 11:04:19 +01:00
|
|
|
</div>
|
|
|
|
|
<div class="col-md-6">
|
2026-08-30 14:34:43 +02:00
|
|
|
<label class="form-label" for="category">Kategori</label>
|
|
|
|
|
<select class="form-select" id="category">
|
|
|
|
|
<option value="general">Generel</option><option value="hardware">Hardware</option>
|
|
|
|
|
<option value="software">Software</option><option value="telecom">Telekom</option>
|
|
|
|
|
<option value="services">Services</option><option value="hosting">Hosting</option>
|
|
|
|
|
</select>
|
2025-12-06 11:04:19 +01:00
|
|
|
</div>
|
|
|
|
|
<div class="col-md-6">
|
2026-08-30 14:34:43 +02:00
|
|
|
<label class="form-label" for="domain">Domæne</label>
|
|
|
|
|
<input type="text" class="form-control" id="domain" placeholder="example.com" autocomplete="off">
|
|
|
|
|
<div class="form-text">Bruges til sikkert mailmatch.</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-12"><div class="form-check form-switch vendor-type-switch">
|
|
|
|
|
<input class="form-check-input" type="checkbox" id="is_internet_provider">
|
|
|
|
|
<label class="form-check-label fw-semibold" for="is_internet_provider">Internetleverandør</label>
|
|
|
|
|
<div class="small text-muted">Leverandøren kan vælges på internetforbindelser og får sin egen forbindelsesfane.</div>
|
|
|
|
|
</div></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-06 11:04:19 +01:00
|
|
|
</div>
|
2026-08-30 14:34:43 +02:00
|
|
|
<div class="col-lg-5">
|
|
|
|
|
<div class="vendor-form-section h-100">
|
|
|
|
|
<div class="vendor-form-section-title"><i class="bi bi-person-lines-fill"></i>Kontakt</div>
|
|
|
|
|
<div class="row g-3">
|
2025-12-06 11:04:19 +01:00
|
|
|
<div class="col-md-6">
|
2026-08-30 14:34:43 +02:00
|
|
|
<label class="form-label" for="email">E-mail</label>
|
|
|
|
|
<input type="email" class="form-control" id="email" autocomplete="email">
|
|
|
|
|
<div class="invalid-feedback">Angiv en gyldig e-mailadresse.</div>
|
2025-12-06 11:04:19 +01:00
|
|
|
</div>
|
|
|
|
|
<div class="col-md-6">
|
2026-08-30 14:34:43 +02:00
|
|
|
<label class="form-label" for="phone">Telefon</label>
|
|
|
|
|
<input type="tel" class="form-control" id="phone" autocomplete="tel">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-12"><label class="form-label" for="website">Website</label><input type="text" class="form-control" id="website" placeholder="https://example.com" autocomplete="url"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-12-06 11:04:19 +01:00
|
|
|
</div>
|
2026-08-30 14:34:43 +02:00
|
|
|
<div class="col-12"><div class="vendor-form-section">
|
|
|
|
|
<div class="vendor-form-section-title"><i class="bi bi-geo-alt"></i>Adresse og noter</div><div class="row g-3">
|
2025-12-06 11:04:19 +01:00
|
|
|
<div class="col-12">
|
2026-08-30 14:34:43 +02:00
|
|
|
<label class="form-label" for="address">Adresse</label>
|
|
|
|
|
<input type="text" class="form-control" id="address" autocomplete="street-address">
|
2025-12-06 11:04:19 +01:00
|
|
|
</div>
|
|
|
|
|
<div class="col-md-3">
|
|
|
|
|
<label class="form-label">Postnummer</label>
|
|
|
|
|
<input type="text" class="form-control" id="postal_code">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-md-5">
|
|
|
|
|
<label class="form-label">By</label>
|
|
|
|
|
<input type="text" class="form-control" id="city">
|
|
|
|
|
</div>
|
2026-08-30 14:34:43 +02:00
|
|
|
<div class="col-md-4 d-flex align-items-end"><div class="form-check form-switch mb-2"><input class="form-check-input" type="checkbox" id="is_active" checked><label class="form-check-label" for="is_active">Aktiv leverandør</label></div></div>
|
2025-12-06 11:04:19 +01:00
|
|
|
<div class="col-12">
|
|
|
|
|
<label class="form-label">Noter</label>
|
|
|
|
|
<textarea class="form-control" id="notes" rows="3"></textarea>
|
|
|
|
|
</div>
|
2026-08-30 14:34:43 +02:00
|
|
|
</div>
|
|
|
|
|
</div></div>
|
2025-12-06 11:04:19 +01:00
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuller</button>
|
2026-08-30 14:34:43 +02:00
|
|
|
<button type="submit" form="createVendorForm" class="btn btn-primary" id="createVendorSubmitBtn">
|
|
|
|
|
<span class="spinner-border spinner-border-sm me-2 d-none" id="createVendorSpinner"></span><i class="bi bi-check-lg me-2" id="createVendorIcon"></i>Opret leverandør
|
2025-12-06 11:04:19 +01:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block extra_js %}
|
|
|
|
|
<script>
|
|
|
|
|
let currentPage = 0;
|
|
|
|
|
let pageSize = 50;
|
|
|
|
|
let currentFilter = 'all';
|
|
|
|
|
let searchTerm = '';
|
|
|
|
|
|
|
|
|
|
async function loadVendors() {
|
|
|
|
|
try {
|
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
|
skip: currentPage * pageSize,
|
|
|
|
|
limit: pageSize
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (searchTerm) {
|
|
|
|
|
params.append('search', searchTerm);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentFilter !== 'all') {
|
|
|
|
|
params.append('category', currentFilter);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 09:15:52 +01:00
|
|
|
console.log('🔄 Loading vendors from:', `/api/v1/vendors?${params}`);
|
2025-12-06 11:04:19 +01:00
|
|
|
const response = await fetch(`/api/v1/vendors?${params}`);
|
2025-12-08 09:15:52 +01:00
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 11:04:19 +01:00
|
|
|
const vendors = await response.json();
|
2025-12-08 09:15:52 +01:00
|
|
|
console.log('✅ Loaded vendors:', vendors.length);
|
2025-12-06 11:04:19 +01:00
|
|
|
|
|
|
|
|
displayVendors(vendors);
|
|
|
|
|
updatePagination(vendors.length);
|
|
|
|
|
} catch (error) {
|
2025-12-08 09:15:52 +01:00
|
|
|
console.error('❌ Error loading vendors:', error);
|
2025-12-06 11:04:19 +01:00
|
|
|
document.getElementById('vendorsTableBody').innerHTML = `
|
|
|
|
|
<tr><td colspan="7" class="text-center text-danger py-5">
|
|
|
|
|
<i class="bi bi-exclamation-triangle fs-2 d-block mb-2"></i>
|
2025-12-08 09:15:52 +01:00
|
|
|
<strong>Kunne ikke indlæse leverandører</strong><br>
|
|
|
|
|
<small class="text-muted">${error.message}</small>
|
2025-12-06 11:04:19 +01:00
|
|
|
</td></tr>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function displayVendors(vendors) {
|
|
|
|
|
const tbody = document.getElementById('vendorsTableBody');
|
|
|
|
|
|
|
|
|
|
if (vendors.length === 0) {
|
|
|
|
|
tbody.innerHTML = `
|
|
|
|
|
<tr><td colspan="7" class="text-center text-muted py-5">
|
|
|
|
|
<i class="bi bi-inbox fs-2 d-block mb-2"></i>
|
|
|
|
|
Ingen leverandører fundet
|
|
|
|
|
</td></tr>
|
|
|
|
|
`;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tbody.innerHTML = vendors.map(vendor => `
|
|
|
|
|
<tr onclick="window.location.href='/vendors/${vendor.id}'" style="cursor: pointer;">
|
|
|
|
|
<td>
|
|
|
|
|
<div class="d-flex align-items-center gap-3">
|
|
|
|
|
<div class="vendor-avatar">${getInitials(vendor.name)}</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="fw-semibold">${escapeHtml(vendor.name)}</div>
|
|
|
|
|
${vendor.domain ? `<small class="text-muted">${escapeHtml(vendor.domain)}</small>` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
|
|
|
|
${vendor.email ? `<div><i class="bi bi-envelope me-2"></i>${escapeHtml(vendor.email)}</div>` : ''}
|
|
|
|
|
${vendor.phone ? `<div><i class="bi bi-telephone me-2"></i>${escapeHtml(vendor.phone)}</div>` : ''}
|
|
|
|
|
${!vendor.email && !vendor.phone ? '<span class="text-muted">-</span>' : ''}
|
|
|
|
|
</td>
|
|
|
|
|
<td>${vendor.cvr_number ? escapeHtml(vendor.cvr_number) : '<span class="text-muted">-</span>'}</td>
|
|
|
|
|
<td>
|
|
|
|
|
<span class="category-badge bg-light">
|
|
|
|
|
${getCategoryIcon(vendor.category)} ${escapeHtml(vendor.category)}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
|
|
|
|
<span class="badge ${vendor.is_active ? 'bg-success' : 'bg-secondary'}">
|
|
|
|
|
${vendor.is_active ? 'Aktiv' : 'Inaktiv'}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td class="text-end">
|
|
|
|
|
<button class="btn btn-sm btn-light" onclick="event.stopPropagation(); editVendor(${vendor.id})">
|
|
|
|
|
<i class="bi bi-pencil"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCategoryIcon(category) {
|
|
|
|
|
const icons = {
|
|
|
|
|
hardware: '🖥️',
|
|
|
|
|
software: '💻',
|
|
|
|
|
telecom: '📡',
|
|
|
|
|
services: '🛠️',
|
|
|
|
|
hosting: '☁️',
|
|
|
|
|
general: '📦'
|
|
|
|
|
};
|
|
|
|
|
return icons[category] || '📦';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getInitials(name) {
|
|
|
|
|
return name.split(' ').map(word => word[0]).join('').substring(0, 2).toUpperCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function escapeHtml(text) {
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.textContent = text;
|
|
|
|
|
return div.innerHTML;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setFilter(filter) {
|
|
|
|
|
currentFilter = filter;
|
|
|
|
|
currentPage = 0;
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
|
|
|
btn.classList.toggle('active', btn.dataset.filter === filter);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
loadVendors();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updatePagination(count) {
|
|
|
|
|
document.getElementById('showingStart').textContent = currentPage * pageSize + 1;
|
|
|
|
|
document.getElementById('showingEnd').textContent = currentPage * pageSize + count;
|
|
|
|
|
document.getElementById('totalCount').textContent = count;
|
|
|
|
|
|
|
|
|
|
document.getElementById('prevBtn').disabled = currentPage === 0;
|
|
|
|
|
document.getElementById('nextBtn').disabled = count < pageSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function previousPage() {
|
|
|
|
|
if (currentPage > 0) {
|
|
|
|
|
currentPage--;
|
|
|
|
|
loadVendors();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function nextPage() {
|
|
|
|
|
currentPage++;
|
|
|
|
|
loadVendors();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showCreateVendorModal() {
|
2026-08-30 14:34:43 +02:00
|
|
|
const form = document.getElementById('createVendorForm');
|
|
|
|
|
form.reset();
|
|
|
|
|
form.classList.remove('was-validated');
|
|
|
|
|
document.getElementById('is_active').checked = true;
|
|
|
|
|
setVendorCvrStatus('Indtast CVR og klik Hent for autofyld.');
|
|
|
|
|
setCreateVendorFeedback();
|
2025-12-06 11:04:19 +01:00
|
|
|
const modal = new bootstrap.Modal(document.getElementById('createVendorModal'));
|
|
|
|
|
modal.show();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-30 14:34:43 +02:00
|
|
|
function setVendorCvrStatus(message, isError = false, isSuccess = false) {
|
|
|
|
|
const status = document.getElementById('vendorCvrLookupStatus');
|
|
|
|
|
status.textContent = message;
|
|
|
|
|
status.className = `form-text${isError ? ' text-danger' : ''}${isSuccess ? ' text-success' : ''}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyVendorCvrData(data) {
|
|
|
|
|
if (data.name) document.getElementById('name').value = data.name;
|
|
|
|
|
if (data.email) document.getElementById('email').value = data.email;
|
|
|
|
|
if (data.phone) document.getElementById('phone').value = data.phone;
|
|
|
|
|
if (data.address) document.getElementById('address').value = data.address;
|
|
|
|
|
if (data.postal_code || data.zipcode) document.getElementById('postal_code').value = data.postal_code || data.zipcode;
|
|
|
|
|
if (data.city) document.getElementById('city').value = data.city;
|
|
|
|
|
if (data.website) document.getElementById('website').value = data.website;
|
|
|
|
|
const domain = normalizeDomain(data.domain || data.website || (data.email?.split('@')[1] || ''));
|
|
|
|
|
if (domain) document.getElementById('domain').value = domain;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function lookupVendorCvr() {
|
|
|
|
|
const input = document.getElementById('cvr_number');
|
|
|
|
|
const button = document.getElementById('vendorCvrLookupBtn');
|
|
|
|
|
const cvr = input.value.replace(/\D/g, '');
|
|
|
|
|
input.value = cvr;
|
|
|
|
|
if (cvr.length !== 8) {
|
|
|
|
|
input.classList.add('is-invalid');
|
|
|
|
|
setVendorCvrStatus('CVR skal være præcis 8 cifre.', true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
input.classList.remove('is-invalid');
|
|
|
|
|
button.disabled = true;
|
|
|
|
|
button.innerHTML = '<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>';
|
|
|
|
|
setVendorCvrStatus('Henter data fra FirmaAPI…');
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`/api/v1/cvr/${cvr}`);
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
if (response.status === 404) throw new Error('CVR blev ikke fundet.');
|
|
|
|
|
const payload = await response.json().catch(() => ({}));
|
|
|
|
|
throw new Error(payload.detail || `Opslaget fejlede (HTTP ${response.status}).`);
|
|
|
|
|
}
|
|
|
|
|
applyVendorCvrData(await response.json());
|
|
|
|
|
setVendorCvrStatus('CVR-data hentet og felter autofyldt.', false, true);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
setVendorCvrStatus(error.message || 'Kunne ikke hente CVR-data.', true);
|
|
|
|
|
} finally {
|
|
|
|
|
button.disabled = false;
|
|
|
|
|
button.textContent = 'Hent';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setCreateVendorFeedback(message = '', type = 'danger') {
|
|
|
|
|
const feedback = document.getElementById('createVendorFeedback');
|
|
|
|
|
feedback.textContent = message;
|
|
|
|
|
feedback.className = message ? `alert alert-${type}` : 'alert d-none';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeDomain(value) {
|
|
|
|
|
return String(value || '').trim().toLowerCase()
|
|
|
|
|
.replace(/^https?:\/\//, '').replace(/^www\./, '').split('/')[0].replace(/\.+$/, '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createVendor(event) {
|
|
|
|
|
event?.preventDefault();
|
2025-12-06 11:04:19 +01:00
|
|
|
const form = document.getElementById('createVendorForm');
|
2026-08-30 14:34:43 +02:00
|
|
|
const cvrInput = document.getElementById('cvr_number');
|
|
|
|
|
cvrInput.value = cvrInput.value.replace(/\D/g, '');
|
|
|
|
|
form.classList.add('was-validated');
|
|
|
|
|
if (!form.checkValidity()) {
|
|
|
|
|
form.querySelector(':invalid')?.focus();
|
|
|
|
|
setCreateVendorFeedback('Kontrollér de markerede felter.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const websiteValue = document.getElementById('website').value.trim();
|
|
|
|
|
const submitButton = document.getElementById('createVendorSubmitBtn');
|
|
|
|
|
submitButton.disabled = true;
|
|
|
|
|
document.getElementById('createVendorSpinner').classList.remove('d-none');
|
|
|
|
|
document.getElementById('createVendorIcon').classList.add('d-none');
|
|
|
|
|
setCreateVendorFeedback('Opretter leverandøren…', 'info');
|
2025-12-06 11:04:19 +01:00
|
|
|
|
|
|
|
|
const vendor = {
|
2026-08-30 14:34:43 +02:00
|
|
|
name: document.getElementById('name').value.trim(),
|
|
|
|
|
cvr_number: cvrInput.value || null,
|
|
|
|
|
email: document.getElementById('email').value.trim().toLowerCase() || null,
|
|
|
|
|
phone: document.getElementById('phone').value.trim() || null,
|
|
|
|
|
website: websiteValue ? (/^https?:\/\//i.test(websiteValue) ? websiteValue : `https://${websiteValue}`) : null,
|
|
|
|
|
domain: normalizeDomain(document.getElementById('domain').value) || null,
|
|
|
|
|
address: document.getElementById('address').value.trim() || null,
|
|
|
|
|
postal_code: document.getElementById('postal_code').value.trim() || null,
|
|
|
|
|
city: document.getElementById('city').value.trim() || null,
|
2025-12-06 11:04:19 +01:00
|
|
|
category: document.getElementById('category').value,
|
2026-08-30 14:34:43 +02:00
|
|
|
notes: document.getElementById('notes').value.trim() || null,
|
|
|
|
|
is_active: document.getElementById('is_active').checked,
|
|
|
|
|
is_internet_provider: document.getElementById('is_internet_provider').checked
|
2025-12-06 11:04:19 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/v1/vendors', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(vendor)
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-30 14:34:43 +02:00
|
|
|
const payload = await response.json().catch(() => ({}));
|
|
|
|
|
if (!response.ok) throw new Error(payload.detail || 'Leverandøren kunne ikke oprettes.');
|
|
|
|
|
setCreateVendorFeedback('Leverandøren er oprettet. Åbner leverandørkortet…', 'success');
|
|
|
|
|
window.setTimeout(() => { window.location.href = `/vendors/${payload.id}`; }, 350);
|
2025-12-06 11:04:19 +01:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error creating vendor:', error);
|
2026-08-30 14:34:43 +02:00
|
|
|
setCreateVendorFeedback(error.message || 'Kunne ikke oprette leverandør.');
|
|
|
|
|
submitButton.disabled = false;
|
|
|
|
|
document.getElementById('createVendorSpinner').classList.add('d-none');
|
|
|
|
|
document.getElementById('createVendorIcon').classList.remove('d-none');
|
2025-12-06 11:04:19 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Search
|
2025-12-08 09:15:52 +01:00
|
|
|
let vendorSearchTimeout;
|
2025-12-06 11:04:19 +01:00
|
|
|
document.getElementById('searchInput').addEventListener('input', (e) => {
|
2025-12-08 09:15:52 +01:00
|
|
|
clearTimeout(vendorSearchTimeout);
|
|
|
|
|
vendorSearchTimeout = setTimeout(() => {
|
2025-12-06 11:04:19 +01:00
|
|
|
searchTerm = e.target.value;
|
|
|
|
|
currentPage = 0;
|
|
|
|
|
loadVendors();
|
|
|
|
|
}, 300);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Load on page ready
|
2026-08-30 14:34:43 +02:00
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
document.getElementById('createVendorForm').addEventListener('submit', createVendor);
|
|
|
|
|
document.getElementById('cvr_number').addEventListener('input', (event) => {
|
|
|
|
|
event.target.value = event.target.value.replace(/\D/g, '').slice(0, 8);
|
|
|
|
|
event.target.classList.remove('is-invalid');
|
|
|
|
|
setVendorCvrStatus('Indtast CVR og klik Hent for autofyld.');
|
|
|
|
|
});
|
|
|
|
|
document.getElementById('cvr_number').addEventListener('keydown', (event) => {
|
|
|
|
|
if (event.key === 'Enter') { event.preventDefault(); lookupVendorCvr(); }
|
|
|
|
|
});
|
|
|
|
|
loadVendors();
|
|
|
|
|
});
|
2025-12-06 11:04:19 +01:00
|
|
|
</script>
|
|
|
|
|
{% endblock %}
|