2025-12-06 02:22:01 +01:00
|
|
|
{% extends "shared/frontend/base.html" %}
|
|
|
|
|
|
|
|
|
|
{% block title %}Kunder - 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-btn:hover, .filter-btn.active {
|
|
|
|
|
background: var(--accent);
|
|
|
|
|
color: white;
|
|
|
|
|
border-color: var(--accent);
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
{% block content %}
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-5">
|
|
|
|
|
<div>
|
|
|
|
|
<h2 class="fw-bold mb-1">Kunder</h2>
|
|
|
|
|
<p class="text-muted mb-0">Administrer dine kunder</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="d-flex gap-3">
|
2025-12-16 15:36:11 +01:00
|
|
|
<input type="text" id="searchInput" class="header-search" placeholder="Søg kunde...">
|
|
|
|
|
<button class="btn btn-primary"><i class="bi bi-plus-lg me-2"></i>Opret Kunde</button>
|
2025-12-06 02:22:01 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-16 15:36:11 +01:00
|
|
|
<div class="mb-4 d-flex gap-2">
|
|
|
|
|
<button class="filter-btn active">Alle Kunder</button>
|
|
|
|
|
<button class="filter-btn">Aktive</button>
|
|
|
|
|
<button class="filter-btn">Inaktive</button>
|
|
|
|
|
<button class="filter-btn">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>
|
|
|
|
|
|
|
|
|
|
<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;
|
2025-12-06 02:22:01 +01:00
|
|
|
|
|
|
|
|
// Load customers on page load
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
loadCustomers();
|
|
|
|
|
|
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');
|
|
|
|
|
searchInput.addEventListener('input', (e) => {
|
2025-12-06 02:22:01 +01:00
|
|
|
clearTimeout(searchTimeout);
|
|
|
|
|
searchTimeout = setTimeout(() => {
|
2025-12-16 15:36:11 +01:00
|
|
|
searchTerm = e.target.value;
|
|
|
|
|
loadCustomers(1);
|
2025-12-06 02:22:01 +01:00
|
|
|
}, 300);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-16 15:36:11 +01:00
|
|
|
async function loadCustomers(page = 1) {
|
|
|
|
|
currentPage = page;
|
|
|
|
|
const offset = (page - 1) * pageSize;
|
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
|
|
|
}
|
2025-12-16 15:36:11 +01:00
|
|
|
const response = await fetch(url);
|
2025-12-06 02:22:01 +01:00
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
totalCustomers = data.total;
|
2025-12-16 15:36:11 +01:00
|
|
|
renderCustomers(data.customers);
|
|
|
|
|
renderPagination();
|
|
|
|
|
updateCount();
|
2025-12-06 02:22:01 +01:00
|
|
|
|
|
|
|
|
} catch (error) {
|
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>
|
|
|
|
|
`;
|
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>';
|
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);">
|
|
|
|
|
${initials}
|
|
|
|
|
</div>
|
2025-12-06 02:22:01 +01:00
|
|
|
<div>
|
2025-12-16 15:36:11 +01:00
|
|
|
<div class="fw-bold">${customer.name || '-'}</div>
|
|
|
|
|
<div class="small text-muted">${customer.address || '-'}</div>
|
2025-12-06 02:22:01 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
<td>
|
2025-12-16 15:36:11 +01:00
|
|
|
<div class="fw-medium">${customer.contact_name || '-'}</div>
|
|
|
|
|
<div class="small text-muted">${customer.contact_phone || '-'}</div>
|
2025-12-06 02:22:01 +01:00
|
|
|
</td>
|
|
|
|
|
<td class="text-muted">${customer.cvr_number || '-'}</td>
|
|
|
|
|
<td>${statusBadge}</td>
|
2025-12-16 15:36:11 +01:00
|
|
|
<td class="text-muted">${customer.email || '-'}</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() {
|
|
|
|
|
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 %}
|