2025-12-06 02:22:01 +01:00
{% extends "shared/frontend/base.html" %}
{% block title %}Kunder - BMC Hub{% endblock %}
{% block extra_css %}
< style >
2026-04-01 21:34:58 +02:00
.customers-toolbar {
gap: 1rem;
}
.toolbar-search-slot {
flex: 1;
display: flex;
justify-content: center;
}
.search-wrap {
position: relative;
min-width: 280px;
max-width: 460px;
width: min(46vw, 460px);
}
.search-wrap .header-search {
width: 100%;
padding-right: 2.4rem;
}
.search-clear {
position: absolute;
right: 0.45rem;
top: 50%;
transform: translateY(-50%);
border: 0;
width: 1.8rem;
height: 1.8rem;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
background: transparent;
}
.search-clear:hover {
background: rgba(15, 76, 117, 0.12);
color: var(--text-primary);
}
.search-clear.d-none {
display: none !important;
}
2025-12-06 02:22:01 +01:00
.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;
}
.filter-btn:hover, .filter-btn.active {
background: var(--accent);
color: white;
border-color: var(--accent);
}
2026-04-01 21:34:58 +02:00
.lookup-status {
font-size: 0.85rem;
color: var(--text-secondary);
}
@media (max-width: 992px) {
.customers-toolbar {
width: 100%;
flex-direction: column;
align-items: stretch !important;
}
.toolbar-search-slot {
width: 100%;
justify-content: stretch;
}
.search-wrap {
width: 100%;
max-width: 100%;
}
}
2025-12-06 02:22:01 +01:00
< / style >
{% endblock %}
{% block content %}
2026-04-01 21:34:58 +02:00
< div class = "d-flex justify-content-between align-items-center mb-5 customers-toolbar" >
2025-12-06 02:22:01 +01:00
< div >
< h2 class = "fw-bold mb-1" > Kunder< / h2 >
< p class = "text-muted mb-0" > Administrer dine kunder< / p >
< / div >
2026-04-01 21:34:58 +02:00
< div class = "toolbar-search-slot" >
< div class = "search-wrap" >
< input type = "search" id = "searchInput" class = "header-search" placeholder = "Søg kunde, CVR, kontakt eller e-mail..." autocomplete = "off" spellcheck = "false" >
< button type = "button" id = "searchClearBtn" class = "search-clear d-none" aria-label = "Ryd søgning" title = "Ryd søgning" >
< i class = "bi bi-x-lg" > < / i >
< / button >
< / div >
2025-12-06 02:22:01 +01:00
< / div >
2026-09-25 11:33:53 +02:00
< div class = "d-flex gap-2" >
< button type = "button" class = "btn btn-outline-secondary" onclick = "openDuplicateCustomersModal()" >
< i class = "bi bi-intersect me-2" > < / i > Find dubletter
< / button >
< button type = "button" id = "openCreateCustomerBtn" class = "btn btn-primary" data-bs-toggle = "modal" data-bs-target = "#createCustomerModal" >
< i class = "bi bi-plus-lg me-2" > < / i > Opret Kunde
< / button >
< / div >
2025-12-06 02:22:01 +01:00
< / div >
2025-12-16 15:36:11 +01:00
< div class = "mb-4 d-flex gap-2" >
2026-04-01 21:34:58 +02:00
< button class = "filter-btn active" data-filter = "all" type = "button" > Alle Kunder< / button >
< button class = "filter-btn" data-filter = "active" type = "button" > Aktive< / button >
< button class = "filter-btn" data-filter = "inactive" type = "button" > Inaktive< / button >
< button class = "filter-btn" data-filter = "vip" type = "button" > VIP< / button >
2025-12-06 02:22:01 +01:00
< / div >
< div class = "card p-4" >
< div class = "table-responsive" >
< table class = "table table-hover align-middle" >
< thead >
< tr >
< th > Virksomhed< / th >
2025-12-16 15:36:11 +01:00
< th > Kontakt< / th >
2025-12-06 02:22:01 +01:00
< th > CVR< / th >
< th > Status< / th >
2025-12-16 15:36:11 +01:00
< th > E-mail< / th >
2025-12-06 02:22:01 +01:00
< th class = "text-end" > Handlinger< / th >
< / tr >
< / thead >
< tbody id = "customersTableBody" >
< tr >
2025-12-16 15:36:11 +01:00
< td colspan = "6" class = "text-center py-5" >
2025-12-06 02:22:01 +01:00
< div class = "spinner-border text-primary" role = "status" >
< span class = "visually-hidden" > Loading...< / span >
< / div >
< / td >
< / tr >
< / tbody >
< / table >
< / div >
2025-12-16 15:36:11 +01:00
< div class = "d-flex justify-content-between align-items-center mt-3" >
< div class = "text-muted" id = "customerCount" > Loading...< / div >
< nav >
< ul class = "pagination mb-0" id = "pagination" > < / ul >
< / nav >
2025-12-06 02:22:01 +01:00
< / div >
< / div >
2026-09-25 11:33:53 +02:00
< div class = "modal fade" id = "duplicateCustomersModal" tabindex = "-1" aria-labelledby = "duplicateCustomersModalLabel" aria-hidden = "true" >
< div class = "modal-dialog modal-xl modal-dialog-scrollable" >
< div class = "modal-content" >
< div class = "modal-header" >
< div > < h5 class = "modal-title" id = "duplicateCustomersModalLabel" > Mulige kundedubletter< / h5 > < small class = "text-muted" > Kun oversigt. Intet flyttes eller slettes her.< / small > < / div >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" aria-label = "Luk" > < / button >
< / div >
< div class = "modal-body" id = "duplicateCustomersContent" > < div class = "text-center py-5" > < div class = "spinner-border text-primary" > < / div > < / div > < / div >
< / div >
< / div >
< / div >
2026-04-01 21:34:58 +02:00
< div class = "modal fade" id = "createCustomerModal" tabindex = "-1" aria-labelledby = "createCustomerModalLabel" aria-hidden = "true" >
< div class = "modal-dialog modal-lg modal-dialog-scrollable" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" id = "createCustomerModalLabel" > Opret ny kunde< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" aria-label = "Luk" > < / button >
< / div >
< form id = "createCustomerForm" >
< div class = "modal-body" >
< div class = "row g-3" >
< div class = "col-md-4" >
< label class = "form-label" for = "createCustomerCvr" > CVR< / label >
< div class = "input-group" >
< input type = "text" class = "form-control" id = "createCustomerCvr" placeholder = "fx 24256790" inputmode = "numeric" maxlength = "8" >
< button type = "button" class = "btn btn-outline-secondary" id = "lookupCvrBtn" > Hent< / button >
< / div >
< div class = "lookup-status mt-1" id = "lookupCvrStatus" > Indtast CVR og klik Hent for autofyld.< / div >
< / div >
< div class = "col-md-8" >
< label class = "form-label" for = "createCustomerName" > Virksomhedsnavn *< / label >
< input type = "text" class = "form-control" id = "createCustomerName" required >
< / div >
< div class = "col-md-6" >
< label class = "form-label" for = "createCustomerEmail" > E-mail< / label >
< input type = "email" class = "form-control" id = "createCustomerEmail" >
< / div >
< div class = "col-md-6" >
< label class = "form-label" for = "createCustomerInvoiceEmail" > Faktura e-mail< / label >
< input type = "email" class = "form-control" id = "createCustomerInvoiceEmail" >
< / div >
< div class = "col-md-6" >
< label class = "form-label" for = "createCustomerPhone" > Telefon< / label >
< input type = "text" class = "form-control" id = "createCustomerPhone" >
< / div >
< div class = "col-md-6" >
< label class = "form-label" for = "createCustomerWebsite" > Website< / label >
< input type = "url" class = "form-control" id = "createCustomerWebsite" placeholder = "https://..." >
< / div >
< div class = "col-md-8" >
< label class = "form-label" for = "createCustomerAddress" > Adresse< / label >
< input type = "text" class = "form-control" id = "createCustomerAddress" >
< / div >
< div class = "col-md-2" >
< label class = "form-label" for = "createCustomerPostalCode" > Postnr.< / label >
< input type = "text" class = "form-control" id = "createCustomerPostalCode" >
< / div >
< div class = "col-md-2" >
< label class = "form-label" for = "createCustomerCity" > By< / label >
< input type = "text" class = "form-control" id = "createCustomerCity" >
< / div >
< div class = "col-md-4" >
< label class = "form-label" for = "createCustomerCountry" > Land< / label >
< input type = "text" class = "form-control" id = "createCustomerCountry" value = "DK" >
< / div >
< div class = "col-md-8 d-flex align-items-end" >
< div class = "form-check form-switch" >
< input class = "form-check-input" type = "checkbox" role = "switch" id = "createCustomerIsActive" checked >
< label class = "form-check-label" for = "createCustomerIsActive" > Kunden er aktiv< / label >
< / div >
< / div >
< / div >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-outline-secondary" data-bs-dismiss = "modal" > Annuller< / button >
< button type = "submit" class = "btn btn-primary" id = "createCustomerSubmitBtn" >
< span class = "submit-label" > Opret kunde< / span >
< / button >
< / div >
< / form >
< / div >
< / div >
< / div >
2025-12-06 02:22:01 +01:00
< script >
2025-12-16 15:36:11 +01:00
let currentPage = 1;
const pageSize = 50;
2025-12-06 02:22:01 +01:00
let totalCustomers = 0;
2025-12-16 15:36:11 +01:00
let searchTerm = '';
let searchTimeout = null;
2026-04-01 21:34:58 +02:00
let currentRequestController = null;
let lastLoadedQueryKey = '';
let createCustomerModal = null;
let activeFilter = 'all';
2025-12-06 02:22:01 +01:00
// Load customers on page load
document.addEventListener('DOMContentLoaded', () => {
loadCustomers();
2026-04-01 21:34:58 +02:00
createCustomerModal = new bootstrap.Modal(document.getElementById('createCustomerModal'));
2025-12-16 15:36:11 +01:00
// Setup search with debounce
2025-12-13 12:06:28 +01:00
const searchInput = document.getElementById('searchInput');
2026-04-01 21:34:58 +02:00
const clearBtn = document.getElementById('searchClearBtn');
const triggerSearch = () => {
const nextSearchTerm = searchInput.value.trim();
if (nextSearchTerm === searchTerm) {
toggleClearButton(nextSearchTerm);
return;
}
searchTerm = nextSearchTerm;
toggleClearButton(searchTerm);
loadCustomers(1);
};
2025-12-13 12:06:28 +01:00
searchInput.addEventListener('input', (e) => {
2025-12-06 02:22:01 +01:00
clearTimeout(searchTimeout);
2026-04-01 21:34:58 +02:00
toggleClearButton(e.target.value.trim());
2025-12-06 02:22:01 +01:00
searchTimeout = setTimeout(() => {
2026-04-01 21:34:58 +02:00
triggerSearch();
2025-12-06 02:22:01 +01:00
}, 300);
});
2026-04-01 21:34:58 +02:00
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
clearTimeout(searchTimeout);
triggerSearch();
}
if (e.key === 'Escape') {
if (!searchInput.value) {
return;
}
searchInput.value = '';
clearTimeout(searchTimeout);
triggerSearch();
}
});
clearBtn.addEventListener('click', () => {
if (!searchInput.value) {
return;
}
searchInput.value = '';
clearTimeout(searchTimeout);
triggerSearch();
searchInput.focus();
});
document.getElementById('createCustomerForm').addEventListener('submit', createCustomer);
document.getElementById('lookupCvrBtn').addEventListener('click', lookupCvrAndAutofill);
document.getElementById('createCustomerCvr').addEventListener('input', onCvrInput);
document.querySelectorAll('.filter-btn[data-filter]').forEach((btn) => {
btn.addEventListener('click', () => {
const nextFilter = btn.dataset.filter || 'all';
if (nextFilter === activeFilter) {
return;
}
activeFilter = nextFilter;
syncFilterButtons();
lastLoadedQueryKey = '';
loadCustomers(1);
});
});
document.getElementById('createCustomerModal').addEventListener('hidden.bs.modal', () => {
resetCreateCustomerForm();
});
2025-12-06 02:22:01 +01:00
});
2026-04-01 21:34:58 +02:00
function onCvrInput(e) {
const digits = String(e.target.value || '').replace(/\D/g, '').slice(0, 8);
e.target.value = digits;
setLookupStatus('Indtast CVR og klik Hent for autofyld.', false);
}
function setLookupStatus(message, isError = false) {
const status = document.getElementById('lookupCvrStatus');
status.textContent = message;
status.classList.toggle('text-danger', isError);
}
async function lookupCvrAndAutofill() {
const cvrInput = document.getElementById('createCustomerCvr');
const lookupBtn = document.getElementById('lookupCvrBtn');
const cvr = String(cvrInput.value || '').replace(/\D/g, '');
if (cvr.length !== 8) {
setLookupStatus('CVR skal være præcis 8 cifre.', true);
return;
}
lookupBtn.disabled = true;
lookupBtn.innerHTML = '< span class = "spinner-border spinner-border-sm" role = "status" aria-hidden = "true" > < / span > ';
setLookupStatus('Henter data fra FirmaAPI...', false);
try {
const response = await fetch(`/api/v1/cvr/${cvr}`);
if (!response.ok) {
if (response.status === 404) {
setLookupStatus('CVR blev ikke fundet.', true);
return;
}
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
applyCustomerAutofill(data || {});
setLookupStatus('CVR-data hentet og felter autofyldt.', false);
} catch (error) {
console.error('CVR lookup failed:', error);
setLookupStatus(`Kunne ikke hente CVR-data: ${error.message}`, true);
} finally {
lookupBtn.disabled = false;
lookupBtn.textContent = 'Hent';
}
}
function applyCustomerAutofill(data) {
if (data.name) document.getElementById('createCustomerName').value = data.name;
if (data.email) document.getElementById('createCustomerEmail').value = data.email;
if (data.phone) document.getElementById('createCustomerPhone').value = data.phone;
if (data.address) document.getElementById('createCustomerAddress').value = data.address;
if (data.city) document.getElementById('createCustomerCity').value = data.city;
if (data.postal_code || data.zipcode) {
document.getElementById('createCustomerPostalCode').value = data.postal_code || data.zipcode;
}
if (data.country) document.getElementById('createCustomerCountry').value = data.country;
if (data.website) document.getElementById('createCustomerWebsite').value = data.website;
}
function buildCreateCustomerPayload() {
const email = document.getElementById('createCustomerEmail').value.trim();
const domain = email.includes('@') ? email.split('@').pop().toLowerCase() : null;
const cleanValue = (id) => {
const value = document.getElementById(id).value.trim();
return value || null;
};
return {
name: document.getElementById('createCustomerName').value.trim(),
cvr_number: cleanValue('createCustomerCvr'),
email: email || null,
email_domain: domain,
phone: cleanValue('createCustomerPhone'),
address: cleanValue('createCustomerAddress'),
city: cleanValue('createCustomerCity'),
postal_code: cleanValue('createCustomerPostalCode'),
country: cleanValue('createCustomerCountry') || 'DK',
website: cleanValue('createCustomerWebsite'),
is_active: document.getElementById('createCustomerIsActive').checked,
invoice_email: cleanValue('createCustomerInvoiceEmail'),
mobile_phone: null,
};
}
async function createCustomer(event) {
event.preventDefault();
const submitBtn = document.getElementById('createCustomerSubmitBtn');
const submitLabel = submitBtn.querySelector('.submit-label');
const payload = buildCreateCustomerPayload();
if (!payload.name) {
setLookupStatus('Virksomhedsnavn er påkrævet.', true);
return;
}
submitBtn.disabled = true;
submitLabel.textContent = 'Opretter...';
try {
const response = await fetch('/api/v1/customers', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText || `HTTP ${response.status}`);
}
const created = await response.json();
createCustomerModal.hide();
searchTerm = '';
document.getElementById('searchInput').value = '';
toggleClearButton('');
lastLoadedQueryKey = '';
await loadCustomers(1);
if (created & & created.id) {
window.location.href = `/customers/${created.id}`;
return;
}
} catch (error) {
console.error('Failed to create customer:', error);
setLookupStatus(`Oprettelse fejlede: ${error.message}`, true);
} finally {
submitBtn.disabled = false;
submitLabel.textContent = 'Opret kunde';
}
}
function resetCreateCustomerForm() {
const form = document.getElementById('createCustomerForm');
form.reset();
document.getElementById('createCustomerCountry').value = 'DK';
document.getElementById('createCustomerIsActive').checked = true;
setLookupStatus('Indtast CVR og klik Hent for autofyld.', false);
}
function syncFilterButtons() {
document.querySelectorAll('.filter-btn[data-filter]').forEach((btn) => {
btn.classList.toggle('active', btn.dataset.filter === activeFilter);
});
}
2025-12-16 15:36:11 +01:00
async function loadCustomers(page = 1) {
currentPage = page;
const offset = (page - 1) * pageSize;
2026-04-01 21:34:58 +02:00
if (currentRequestController) {
currentRequestController.abort();
}
currentRequestController = new AbortController();
2025-12-06 02:22:01 +01:00
try {
2025-12-16 15:36:11 +01:00
let url = `/api/v1/customers?limit=${pageSize}&offset=${offset}`;
if (searchTerm) {
url += `&search=${encodeURIComponent(searchTerm)}`;
2025-12-06 02:22:01 +01:00
}
2026-04-01 21:34:58 +02:00
if (activeFilter === 'active') {
url += '&is_active=true';
} else if (activeFilter === 'inactive') {
url += '&is_active=false';
} else if (activeFilter === 'vip') {
url += '&vip=true';
}
const queryKey = `${page}|${searchTerm}|${activeFilter}`;
if (queryKey === lastLoadedQueryKey) {
return;
}
const response = await fetch(url, { signal: currentRequestController.signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
2025-12-06 02:22:01 +01:00
const data = await response.json();
2026-04-01 21:34:58 +02:00
lastLoadedQueryKey = queryKey;
2025-12-06 02:22:01 +01:00
totalCustomers = data.total;
2025-12-16 15:36:11 +01:00
renderCustomers(data.customers);
renderPagination();
updateCount();
2026-04-01 21:34:58 +02:00
2025-12-06 02:22:01 +01:00
} catch (error) {
2026-04-01 21:34:58 +02:00
if (error.name === 'AbortError') {
return;
}
2025-12-16 15:36:11 +01:00
console.error('Error loading customers:', error);
document.getElementById('customersTableBody').innerHTML = `
< tr > < td colspan = "6" class = "text-center text-danger py-5" >
❌ Fejl ved indlæsning: ${error.message}
< / td > < / tr >
`;
2026-04-01 21:34:58 +02:00
} finally {
currentRequestController = null;
}
}
2026-09-25 11:33:53 +02:00
function duplicateEscape(value) {
return escapeHtml(value === null || value === undefined || value === '' ? '– ' : value);
}
function duplicateRelationSummary(counts) {
const labels = { contacts: 'kontakter', sager: 'sager', lokationer: 'lokationer', hardware: 'hardware', abonnementer: 'abonnementer' };
return Object.entries(labels).map(([key, label]) => `${counts?.[key] || 0} ${label}`).join(' · ');
}
async function openDuplicateCustomersModal() {
const modalElement = document.getElementById('duplicateCustomersModal');
const content = document.getElementById('duplicateCustomersContent');
bootstrap.Modal.getOrCreateInstance(modalElement).show();
content.innerHTML = '< div class = "text-center py-5" > < div class = "spinner-border text-primary" > < / div > < div class = "text-muted mt-2" > Finder dubletter…< / div > < / div > ';
try {
const response = await fetch('/api/v1/customers/duplicates', { credentials: 'include' });
const data = await response.json();
if (!response.ok) throw new Error(data.detail || 'Kunne ikke hente dubletter');
const groups = Array.isArray(data.groups) ? data.groups : [];
if (!groups.length) {
content.innerHTML = '< div class = "alert alert-success mb-0" > < i class = "bi bi-check-circle me-2" > < / i > Ingen aktive kunder med samme navn blev fundet.< / div > ';
return;
}
const status = {
same_economic: ['success', 'Samme e-conomic-relation'],
conflicting_economic: ['danger', 'Forskellige e-conomic-relationer – kræver kontrol'],
missing_economic: ['warning', 'Mangler e-conomic-relation']
};
content.innerHTML = `< p class = "text-muted small" > ${groups.length} grupper fundet. Den fremhævede kandidat er kun et forslag baseret på e-conomic-nummer, CVR og oprettelsesdato.< / p > ` + groups.map(group => {
const [colour, label] = status[group.economic_status] || status.missing_economic;
const rows = group.candidates.map((customer, index) => `
< tr class = "${index === 0 ? 'table-light' : ''}" >
< td > < a href = "/customers/${customer.id}" class = "fw-semibold" > ${duplicateEscape(customer.name)}< / a > ${index === 0 ? '< span class = "badge text-bg-primary ms-2" > Foreslået hovedkunde< / span > ' : ''}< / td >
< td > ${duplicateEscape(customer.economic_customer_number)}< / td >
< td > ${duplicateEscape(customer.cvr_number)}< / td >
< td class = "small text-muted" > ${duplicateRelationSummary(customer.relation_counts)}< / td >
< / tr > `).join('');
return `< section class = "border rounded-3 mb-3 overflow-hidden" >
< div class = "d-flex justify-content-between gap-3 align-items-center px-3 py-2 bg-light" > < strong > ${duplicateEscape(group.candidates?.[0]?.name || group.key)}< / strong > < span class = "badge text-bg-${colour}" > ${label}< / span > < / div >
< div class = "table-responsive" > < table class = "table table-sm align-middle mb-0" > < thead > < tr > < th > Kunde< / th > < th > e-conomic< / th > < th > CVR< / th > < th > Data knyttet til posten< / th > < / tr > < / thead > < tbody > ${rows}< / tbody > < / table > < / div >
< / section > `;
}).join('');
} catch (error) {
content.innerHTML = `< div class = "alert alert-danger mb-0" > ${duplicateEscape(error.message)}< / div > `;
}
}
2026-04-01 21:34:58 +02:00
function toggleClearButton(value) {
document.getElementById('searchClearBtn')?.classList.toggle('d-none', !value);
}
function escapeHtml(value) {
if (value === null || value === undefined) {
return '-';
2025-12-06 02:22:01 +01:00
}
2026-04-01 21:34:58 +02:00
return String(value)
.replace(/& /g, '& ')
.replace(/< /g, '< ')
.replace(/>/g, '> ')
.replace(/"/g, '" ')
.replace(/'/g, '' ');
2025-12-06 02:22:01 +01:00
}
2025-12-16 15:36:11 +01:00
function renderCustomers(customers) {
2025-12-06 02:22:01 +01:00
const tbody = document.getElementById('customersTableBody');
if (!customers || customers.length === 0) {
2025-12-16 15:36:11 +01:00
tbody.innerHTML = `
< tr > < td colspan = "6" class = "text-center text-muted py-5" >
Ingen kunder fundet
< / td > < / tr >
`;
2025-12-06 02:22:01 +01:00
return;
}
tbody.innerHTML = customers.map(customer => {
2025-12-16 15:36:11 +01:00
const initials = customer.name ? customer.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase() : '??';
const statusBadge = customer.is_active ?
'< span class = "badge bg-success bg-opacity-10 text-success" > Aktiv< / span > ' :
'< span class = "badge bg-secondary bg-opacity-10 text-secondary" > Inaktiv< / span > ';
2026-04-01 21:34:58 +02:00
const safeInitials = escapeHtml(initials);
const safeName = escapeHtml(customer.name);
const safeAddress = escapeHtml(customer.address);
const safeContactName = escapeHtml(customer.contact_name);
const safeContactPhone = escapeHtml(customer.contact_phone);
const safeCvr = escapeHtml(customer.cvr_number);
const safeEmail = escapeHtml(customer.email);
2025-12-06 02:22:01 +01:00
return `
2025-12-16 15:36:11 +01:00
< tr onclick = "window.location.href='/customers/${customer.id}'" style = "cursor: pointer;" >
2025-12-06 02:22:01 +01:00
< td >
< div class = "d-flex align-items-center" >
2025-12-16 15:36:11 +01:00
< div class = "rounded bg-light d-flex align-items-center justify-content-center me-3 fw-bold"
style="width: 40px; height: 40px; color: var(--accent);">
2026-04-01 21:34:58 +02:00
${safeInitials}
2025-12-16 15:36:11 +01:00
< / div >
2025-12-06 02:22:01 +01:00
< div >
2026-04-01 21:34:58 +02:00
< div class = "fw-bold" > ${safeName}< / div >
< div class = "small text-muted" > ${safeAddress}< / div >
2025-12-06 02:22:01 +01:00
< / div >
< / div >
< / td >
< td >
2026-04-01 21:34:58 +02:00
< div class = "fw-medium" > ${safeContactName}< / div >
< div class = "small text-muted" > ${safeContactPhone}< / div >
2025-12-06 02:22:01 +01:00
< / td >
2026-04-01 21:34:58 +02:00
< td class = "text-muted" > ${safeCvr}< / td >
2025-12-06 02:22:01 +01:00
< td > ${statusBadge}< / td >
2026-04-01 21:34:58 +02:00
< td class = "text-muted" > ${safeEmail}< / td >
2025-12-06 02:22:01 +01:00
< td class = "text-end" >
2025-12-16 15:36:11 +01:00
< button class = "btn btn-sm btn-outline-primary"
onclick="event.stopPropagation(); window.location.href='/customers/${customer.id}'"
title="Se detaljer">
< i class = "bi bi-arrow-right" > < / i >
< / button >
2025-12-06 02:22:01 +01:00
< / td >
< / tr >
`;
}).join('');
}
2025-12-16 15:36:11 +01:00
function renderPagination() {
const totalPages = Math.ceil(totalCustomers / pageSize);
const pagination = document.getElementById('pagination');
2025-12-06 02:22:01 +01:00
2025-12-16 15:36:11 +01:00
if (totalPages < = 1) {
pagination.innerHTML = '';
return;
2025-12-06 02:22:01 +01:00
}
2025-12-16 15:36:11 +01:00
let pages = [];
2025-12-06 02:22:01 +01:00
2025-12-16 15:36:11 +01:00
// Previous button
pages.push(`
< li class = "page-item ${currentPage === 1 ? 'disabled' : ''}" >
< a class = "page-link" href = "#" onclick = "loadCustomers(${currentPage - 1}); return false;" >
< i class = "bi bi-chevron-left" > < / i >
< / a >
< / li >
`);
2025-12-06 02:22:01 +01:00
2025-12-16 15:36:11 +01:00
// Page numbers (show max 7 pages)
let startPage = Math.max(1, currentPage - 3);
let endPage = Math.min(totalPages, startPage + 6);
2025-12-06 02:22:01 +01:00
2025-12-16 15:36:11 +01:00
if (endPage - startPage < 6 ) {
startPage = Math.max(1, endPage - 6);
2025-12-06 02:22:01 +01:00
}
2025-12-16 15:36:11 +01:00
if (startPage > 1) {
pages.push(`< li class = "page-item" > < a class = "page-link" href = "#" onclick = "loadCustomers(1); return false;" > 1< / a > < / li > `);
if (startPage > 2) {
pages.push(`< li class = "page-item disabled" > < span class = "page-link" > ...< / span > < / li > `);
}
2025-12-06 02:22:01 +01:00
}
2025-12-16 15:36:11 +01:00
for (let i = startPage; i < = endPage; i++) {
pages.push(`
< li class = "page-item ${i === currentPage ? 'active' : ''}" >
< a class = "page-link" href = "#" onclick = "loadCustomers(${i}); return false;" > ${i}< / a >
< / li >
`);
}
2025-12-06 02:22:01 +01:00
2025-12-16 15:36:11 +01:00
if (endPage < totalPages ) {
if (endPage < totalPages - 1 ) {
pages.push(`< li class = "page-item disabled" > < span class = "page-link" > ...< / span > < / li > `);
2025-12-06 02:22:01 +01:00
}
2025-12-16 15:36:11 +01:00
pages.push(`< li class = "page-item" > < a class = "page-link" href = "#" onclick = "loadCustomers(${totalPages}); return false;" > ${totalPages}< / a > < / li > `);
2025-12-06 02:22:01 +01:00
}
2025-12-16 15:36:11 +01:00
// Next button
pages.push(`
< li class = "page-item ${currentPage === totalPages ? 'disabled' : ''}" >
< a class = "page-link" href = "#" onclick = "loadCustomers(${currentPage + 1}); return false;" >
< i class = "bi bi-chevron-right" > < / i >
< / a >
< / li >
`);
pagination.innerHTML = pages.join('');
2025-12-06 02:22:01 +01:00
}
2025-12-16 15:36:11 +01:00
function updateCount() {
2026-04-01 21:34:58 +02:00
if (totalCustomers === 0) {
document.getElementById('customerCount').textContent = 'Ingen kunder fundet';
return;
}
2025-12-16 15:36:11 +01:00
const start = (currentPage - 1) * pageSize + 1;
const end = Math.min(currentPage * pageSize, totalCustomers);
document.getElementById('customerCount').textContent =
`Viser ${start}-${end} af ${totalCustomers} kunder`;
2025-12-06 02:22:01 +01:00
}
< / script >
2025-12-16 15:36:11 +01:00
{% endblock %}