bmc_hub/app/customers/frontend/customers.html

503 lines
19 KiB
HTML
Raw Permalink Normal View History

{% 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;
cursor: pointer;
}
.filter-btn:hover, .filter-btn.active {
background: var(--accent);
color: white;
border-color: var(--accent);
}
.customer-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;
}
.pagination-btn {
border: 1px solid rgba(0,0,0,0.1);
padding: 0.5rem 1rem;
background: var(--bg-card);
color: var(--text-primary);
cursor: pointer;
transition: all 0.2s;
}
.pagination-btn:hover:not(:disabled) {
background: var(--accent-light);
border-color: var(--accent);
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</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">
<input type="text" id="searchInput" class="header-search" placeholder="Søg kunde, CVR, email...">
<button class="btn btn-primary" onclick="showCreateCustomerModal()">
<i class="bi bi-plus-lg me-2"></i>Opret Kunde
</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 Kunder <span id="countAll" class="ms-1"></span>
</button>
<button class="filter-btn" data-filter="active" onclick="setFilter('active')">
Aktive <span id="countActive" class="ms-1"></span>
</button>
<button class="filter-btn" data-filter="inactive" onclick="setFilter('inactive')">
Inaktive <span id="countInactive" class="ms-1"></span>
</button>
<button class="filter-btn" data-filter="vtiger" onclick="setFilter('vtiger')">
<i class="bi bi-cloud me-1"></i>vTiger
</button>
<button class="filter-btn" data-filter="local" onclick="setFilter('local')">
<i class="bi bi-hdd me-1"></i>Lokal
</button>
</div>
<div class="card p-4">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Virksomhed</th>
<th>Kontakt Info</th>
<th>CVR</th>
<th>Kilde</th>
<th>Status</th>
<th>Kontakter</th>
<th class="text-end">Handlinger</th>
</tr>
</thead>
<tbody id="customersTableBody">
<tr>
<td colspan="7" class="text-center py-5">
<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> kunder
</div>
<div class="d-flex gap-2">
<button class="pagination-btn" id="prevBtn" onclick="previousPage()">
<i class="bi bi-chevron-left"></i> Forrige
</button>
<button class="pagination-btn" id="nextBtn" onclick="nextPage()">
Næste <i class="bi bi-chevron-right"></i>
</button>
</div>
</div>
</div>
<!-- Create Customer Modal -->
<div class="modal fade" id="createCustomerModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Opret Ny Kunde</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="createCustomerForm">
<!-- CVR Lookup Section -->
<div class="mb-4">
<label class="form-label">CVR-nummer</label>
<div class="input-group">
<input type="text" class="form-control" id="cvrInput" placeholder="12345678" maxlength="8">
<button class="btn btn-primary" type="button" id="cvrLookupBtn" onclick="lookupCVR()">
<i class="bi bi-search me-2"></i>Søg CVR
</button>
</div>
<div class="form-text">Indtast CVR-nummer for automatisk udfyldning</div>
<div id="cvrLookupStatus" class="mt-2"></div>
</div>
<hr class="my-4">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label">Virksomhedsnavn <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="nameInput" required>
</div>
<div class="col-md-8">
<label class="form-label">Adresse</label>
<input type="text" class="form-control" id="addressInput">
</div>
<div class="col-md-4">
<label class="form-label">Postnummer</label>
<input type="text" class="form-control" id="postalCodeInput">
</div>
<div class="col-md-6">
<label class="form-label">By</label>
<input type="text" class="form-control" id="cityInput">
</div>
<div class="col-md-6">
<label class="form-label">Email</label>
<input type="email" class="form-control" id="emailInput">
</div>
<div class="col-md-6">
<label class="form-label">Telefon</label>
<input type="text" class="form-control" id="phoneInput">
</div>
<div class="col-md-6">
<label class="form-label">Hjemmeside</label>
<input type="url" class="form-control" id="websiteInput" placeholder="https://">
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isActiveInput" checked>
<label class="form-check-label" for="isActiveInput">
Aktiv kunde
</label>
</div>
</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="createCustomer()">
<i class="bi bi-plus-lg me-2"></i>Opret Kunde
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let currentPage = 0;
let pageSize = 20;
let currentFilter = 'all';
let searchQuery = '';
let totalCustomers = 0;
// Load customers on page load
document.addEventListener('DOMContentLoaded', () => {
loadCustomers();
// Search with debounce
let searchTimeout;
document.getElementById('searchInput').addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
searchQuery = e.target.value;
currentPage = 0;
loadCustomers();
}, 300);
});
});
function setFilter(filter) {
currentFilter = filter;
currentPage = 0;
// Update active button
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
loadCustomers();
}
async function loadCustomers() {
const tbody = document.getElementById('customersTableBody');
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5"><div class="spinner-border text-primary"></div></td></tr>';
try {
// Build query parameters
let params = new URLSearchParams({
limit: pageSize,
offset: currentPage * pageSize
});
if (searchQuery) {
params.append('search', searchQuery);
}
if (currentFilter === 'active') {
params.append('is_active', 'true');
} else if (currentFilter === 'inactive') {
params.append('is_active', 'false');
} else if (currentFilter === 'vtiger' || currentFilter === 'local') {
params.append('source', currentFilter);
}
const response = await fetch(`/api/v1/customers?${params}`);
const data = await response.json();
totalCustomers = data.total;
displayCustomers(data.customers);
updatePagination(data.total);
} catch (error) {
console.error('Failed to load customers:', error);
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5 text-danger">Kunne ikke indlæse kunder</td></tr>';
}
}
function displayCustomers(customers) {
const tbody = document.getElementById('customersTableBody');
if (!customers || customers.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-5 text-muted">Ingen kunder fundet</td></tr>';
return;
}
tbody.innerHTML = customers.map(customer => {
const initials = getInitials(customer.name);
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>';
const sourceBadge = customer.vtiger_id
? '<span class="badge bg-primary bg-opacity-10 text-primary"><i class="bi bi-cloud me-1"></i>vTiger</span>'
: '<span class="badge bg-secondary bg-opacity-10 text-secondary"><i class="bi bi-hdd me-1"></i>Lokal</span>';
const contactCount = customer.contact_count || 0;
return `
<tr style="cursor: pointer;" onclick="viewCustomer(${customer.id})">
<td>
<div class="d-flex align-items-center">
<div class="customer-avatar me-3">${initials}</div>
<div>
<div class="fw-bold">${escapeHtml(customer.name)}</div>
<div class="small text-muted">${customer.city || customer.address || '-'}</div>
</div>
</div>
</td>
<td>
<div class="fw-medium">${customer.email || '-'}</div>
<div class="small text-muted">${customer.phone || '-'}</div>
</td>
<td class="text-muted">${customer.cvr_number || '-'}</td>
<td>${sourceBadge}</td>
<td>${statusBadge}</td>
<td>
<span class="badge bg-light text-dark border">
<i class="bi bi-person me-1"></i>${contactCount}
</span>
</td>
<td class="text-end">
<div class="btn-group">
<button class="btn btn-sm btn-light" onclick="event.stopPropagation(); viewCustomer(${customer.id})">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-sm btn-light" onclick="event.stopPropagation(); editCustomer(${customer.id})">
<i class="bi bi-pencil"></i>
</button>
</div>
</td>
</tr>
`;
}).join('');
}
function updatePagination(total) {
const start = currentPage * pageSize + 1;
const end = Math.min((currentPage + 1) * pageSize, total);
document.getElementById('showingStart').textContent = total > 0 ? start : 0;
document.getElementById('showingEnd').textContent = end;
document.getElementById('totalCount').textContent = total;
// Update buttons
document.getElementById('prevBtn').disabled = currentPage === 0;
document.getElementById('nextBtn').disabled = end >= total;
}
function previousPage() {
if (currentPage > 0) {
currentPage--;
loadCustomers();
}
}
function nextPage() {
if ((currentPage + 1) * pageSize < totalCustomers) {
currentPage++;
loadCustomers();
}
}
function viewCustomer(customerId) {
window.location.href = `/customers/${customerId}`;
}
function editCustomer(customerId) {
// TODO: Open edit modal
console.log('Edit customer:', customerId);
}
function showCreateCustomerModal() {
// Reset form
document.getElementById('createCustomerForm').reset();
document.getElementById('cvrLookupStatus').innerHTML = '';
document.getElementById('isActiveInput').checked = true;
// Show modal
const modal = new bootstrap.Modal(document.getElementById('createCustomerModal'));
modal.show();
}
async function lookupCVR() {
const cvrInput = document.getElementById('cvrInput');
const cvr = cvrInput.value.trim();
const statusDiv = document.getElementById('cvrLookupStatus');
const lookupBtn = document.getElementById('cvrLookupBtn');
if (!cvr || cvr.length !== 8) {
statusDiv.innerHTML = '<div class="alert alert-warning mb-0"><i class="bi bi-exclamation-triangle me-2"></i>Indtast et gyldigt 8-cifret CVR-nummer</div>';
return;
}
// Show loading state
lookupBtn.disabled = true;
lookupBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Søger...';
statusDiv.innerHTML = '<div class="alert alert-info mb-0"><i class="bi bi-hourglass-split me-2"></i>Henter virksomhedsoplysninger...</div>';
try {
const response = await fetch(`/api/v1/cvr/${cvr}`);
if (!response.ok) {
throw new Error('CVR ikke fundet');
}
const data = await response.json();
// Auto-fill form fields
document.getElementById('nameInput').value = data.name || '';
document.getElementById('addressInput').value = data.address || '';
document.getElementById('postalCodeInput').value = data.postal_code || '';
document.getElementById('cityInput').value = data.city || '';
document.getElementById('phoneInput').value = data.phone || '';
document.getElementById('emailInput').value = data.email || '';
statusDiv.innerHTML = '<div class="alert alert-success mb-0"><i class="bi bi-check-circle me-2"></i>Virksomhedsoplysninger hentet fra CVR-registeret</div>';
} catch (error) {
console.error('CVR lookup failed:', error);
statusDiv.innerHTML = '<div class="alert alert-danger mb-0"><i class="bi bi-x-circle me-2"></i>Kunne ikke finde virksomhed med CVR-nummer ' + cvr + '</div>';
} finally {
lookupBtn.disabled = false;
lookupBtn.innerHTML = '<i class="bi bi-search me-2"></i>Søg CVR';
}
}
async function createCustomer() {
const name = document.getElementById('nameInput').value.trim();
if (!name) {
alert('Virksomhedsnavn er påkrævet');
return;
}
const customerData = {
name: name,
cvr_number: document.getElementById('cvrInput').value.trim() || null,
address: document.getElementById('addressInput').value.trim() || null,
postal_code: document.getElementById('postalCodeInput').value.trim() || null,
city: document.getElementById('cityInput').value.trim() || null,
email: document.getElementById('emailInput').value.trim() || null,
phone: document.getElementById('phoneInput').value.trim() || null,
website: document.getElementById('websiteInput').value.trim() || null,
is_active: document.getElementById('isActiveInput').checked
};
try {
const response = await fetch('/api/v1/customers', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(customerData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Kunne ikke oprette kunde');
}
const newCustomer = await response.json();
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('createCustomerModal'));
modal.hide();
// Reload customer list
await loadCustomers();
// Show success message (optional)
alert('Kunde oprettet succesfuldt!');
} catch (error) {
console.error('Failed to create customer:', error);
alert('Fejl ved oprettelse af kunde: ' + error.message);
}
}
function getInitials(name) {
if (!name) return '?';
const words = name.trim().split(' ');
if (words.length === 1) return words[0].substring(0, 2).toUpperCase();
return (words[0][0] + words[words.length - 1][0]).toUpperCase();
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
{% endblock %}