495 lines
19 KiB
HTML
495 lines
19 KiB
HTML
|
|
{% extends "shared/frontend/base.html" %}
|
||
|
|
|
||
|
|
{% block title %}Kunde Detaljer - BMC Hub{% endblock %}
|
||
|
|
|
||
|
|
{% block extra_css %}
|
||
|
|
<style>
|
||
|
|
.customer-header {
|
||
|
|
background: var(--accent);
|
||
|
|
color: white;
|
||
|
|
padding: 3rem 2rem;
|
||
|
|
border-radius: 12px;
|
||
|
|
margin-bottom: 2rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.customer-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);
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav-pills-vertical {
|
||
|
|
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
||
|
|
padding-right: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav-pills-vertical .nav-link {
|
||
|
|
color: var(--text-secondary);
|
||
|
|
border-radius: 8px 0 0 8px;
|
||
|
|
padding: 1rem 1.5rem;
|
||
|
|
font-weight: 500;
|
||
|
|
margin-bottom: 0.5rem;
|
||
|
|
transition: all 0.2s;
|
||
|
|
text-align: left;
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav-pills-vertical .nav-link:hover {
|
||
|
|
background: var(--accent-light);
|
||
|
|
color: var(--accent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav-pills-vertical .nav-link.active {
|
||
|
|
background: var(--accent);
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav-pills-vertical .nav-link i {
|
||
|
|
width: 20px;
|
||
|
|
margin-right: 0.75rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-card {
|
||
|
|
background: var(--bg-card);
|
||
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||
|
|
border-radius: 12px;
|
||
|
|
padding: 1.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-row {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
padding: 0.75rem 0;
|
||
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-row:last-child {
|
||
|
|
border-bottom: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-label {
|
||
|
|
font-weight: 500;
|
||
|
|
color: var(--text-secondary);
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-value {
|
||
|
|
color: var(--text-primary);
|
||
|
|
font-weight: 500;
|
||
|
|
}
|
||
|
|
|
||
|
|
.contact-card {
|
||
|
|
background: var(--bg-card);
|
||
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||
|
|
border-radius: 12px;
|
||
|
|
padding: 1.5rem;
|
||
|
|
transition: all 0.2s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.contact-card:hover {
|
||
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
|
|
transform: translateY(-2px);
|
||
|
|
}
|
||
|
|
|
||
|
|
.activity-item {
|
||
|
|
padding: 1.5rem;
|
||
|
|
border-left: 3px solid var(--accent-light);
|
||
|
|
margin-left: 1rem;
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
.activity-item::before {
|
||
|
|
content: '';
|
||
|
|
position: absolute;
|
||
|
|
left: -8px;
|
||
|
|
top: 1.5rem;
|
||
|
|
width: 12px;
|
||
|
|
height: 12px;
|
||
|
|
background: var(--accent);
|
||
|
|
border-radius: 50%;
|
||
|
|
border: 3px solid var(--bg-body);
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
{% endblock %}
|
||
|
|
|
||
|
|
{% block content %}
|
||
|
|
<!-- Customer Header -->
|
||
|
|
<div class="customer-header">
|
||
|
|
<div class="d-flex justify-content-between align-items-start">
|
||
|
|
<div class="d-flex align-items-center">
|
||
|
|
<div class="customer-avatar-large me-4" id="customerAvatar">?</div>
|
||
|
|
<div>
|
||
|
|
<h1 class="fw-bold mb-2" id="customerName">Loading...</h1>
|
||
|
|
<div class="d-flex gap-3 align-items-center">
|
||
|
|
<span id="customerCity"></span>
|
||
|
|
<span class="badge bg-white bg-opacity-20" id="customerStatus"></span>
|
||
|
|
<span class="badge bg-white bg-opacity-20" id="customerSource"></span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="d-flex gap-2">
|
||
|
|
<button class="btn btn-light btn-sm" onclick="editCustomer()">
|
||
|
|
<i class="bi bi-pencil me-2"></i>Rediger
|
||
|
|
</button>
|
||
|
|
<button class="btn btn-light btn-sm" onclick="window.location.href='/customers'">
|
||
|
|
<i class="bi bi-arrow-left me-2"></i>Tilbage
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Content Layout with Sidebar Navigation -->
|
||
|
|
<div class="row">
|
||
|
|
<div class="col-lg-3 col-md-4">
|
||
|
|
<!-- Vertical Navigation -->
|
||
|
|
<ul class="nav nav-pills nav-pills-vertical flex-column" role="tablist">
|
||
|
|
<li class="nav-item">
|
||
|
|
<a class="nav-link active" data-bs-toggle="tab" href="#overview">
|
||
|
|
<i class="bi bi-info-circle"></i>Oversigt
|
||
|
|
</a>
|
||
|
|
</li>
|
||
|
|
<li class="nav-item">
|
||
|
|
<a class="nav-link" data-bs-toggle="tab" href="#contacts">
|
||
|
|
<i class="bi bi-people"></i>Kontakter
|
||
|
|
</a>
|
||
|
|
</li>
|
||
|
|
<li class="nav-item">
|
||
|
|
<a class="nav-link" data-bs-toggle="tab" href="#invoices">
|
||
|
|
<i class="bi bi-receipt"></i>Fakturaer
|
||
|
|
</a>
|
||
|
|
</li>
|
||
|
|
<li class="nav-item">
|
||
|
|
<a class="nav-link" data-bs-toggle="tab" href="#hardware">
|
||
|
|
<i class="bi bi-hdd"></i>Hardware
|
||
|
|
</a>
|
||
|
|
</li>
|
||
|
|
<li class="nav-item">
|
||
|
|
<a class="nav-link" data-bs-toggle="tab" href="#activity">
|
||
|
|
<i class="bi bi-clock-history"></i>Aktivitet
|
||
|
|
</a>
|
||
|
|
</li>
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="col-lg-9 col-md-8">
|
||
|
|
<!-- Tab Content -->
|
||
|
|
<div class="tab-content">
|
||
|
|
<!-- Overview Tab -->
|
||
|
|
<div class="tab-pane fade show active" id="overview">
|
||
|
|
<div class="row g-4">
|
||
|
|
<div class="col-lg-6">
|
||
|
|
<div class="info-card">
|
||
|
|
<h5 class="fw-bold mb-4">Virksomhedsoplysninger</h5>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">CVR-nummer</span>
|
||
|
|
<span class="info-value" id="cvrNumber">-</span>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">Adresse</span>
|
||
|
|
<span class="info-value" id="address">-</span>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">Postnummer & By</span>
|
||
|
|
<span class="info-value" id="postalCity">-</span>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">Email</span>
|
||
|
|
<span class="info-value" id="email">-</span>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">Telefon</span>
|
||
|
|
<span class="info-value" id="phone">-</span>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">Hjemmeside</span>
|
||
|
|
<span class="info-value" id="website">-</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="col-lg-6">
|
||
|
|
<div class="info-card">
|
||
|
|
<h5 class="fw-bold mb-4">Økonomiske Oplysninger</h5>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">e-conomic Kundenr.</span>
|
||
|
|
<span class="info-value" id="economicNumber">-</span>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">Betalingsbetingelser</span>
|
||
|
|
<span class="info-value" id="paymentTerms">-</span>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">Moms Zone</span>
|
||
|
|
<span class="info-value" id="vatZone">-</span>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">Valuta</span>
|
||
|
|
<span class="info-value" id="currency">-</span>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">EAN-nummer</span>
|
||
|
|
<span class="info-value" id="ean">-</span>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">Spærret</span>
|
||
|
|
<span class="info-value" id="barred">-</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="col-12">
|
||
|
|
<div class="info-card">
|
||
|
|
<h5 class="fw-bold mb-3">Integration</h5>
|
||
|
|
<div class="row">
|
||
|
|
<div class="col-md-6">
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">vTiger ID</span>
|
||
|
|
<span class="info-value" id="vtigerId">-</span>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">vTiger Sidst Synkroniseret</span>
|
||
|
|
<span class="info-value" id="vtigerSyncAt">-</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="col-md-6">
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">e-conomic Sidst Synkroniseret</span>
|
||
|
|
<span class="info-value" id="economicSyncAt">-</span>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">Oprettet</span>
|
||
|
|
<span class="info-value" id="createdAt">-</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Contacts Tab -->
|
||
|
|
<div class="tab-pane fade" id="contacts">
|
||
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||
|
|
<h5 class="fw-bold mb-0">Kontaktpersoner</h5>
|
||
|
|
<button class="btn btn-primary btn-sm" onclick="showAddContactModal()">
|
||
|
|
<i class="bi bi-plus-lg me-2"></i>Tilføj Kontakt
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div class="row g-4" id="contactsContainer">
|
||
|
|
<div class="col-12 text-center py-5">
|
||
|
|
<div class="spinner-border text-primary"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Invoices Tab -->
|
||
|
|
<div class="tab-pane fade" id="invoices">
|
||
|
|
<h5 class="fw-bold mb-4">Fakturaer</h5>
|
||
|
|
<div class="text-muted text-center py-5">
|
||
|
|
Fakturamodul kommer snart...
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Hardware Tab -->
|
||
|
|
<div class="tab-pane fade" id="hardware">
|
||
|
|
<h5 class="fw-bold mb-4">Hardware</h5>
|
||
|
|
<div class="text-muted text-center py-5">
|
||
|
|
Hardwaremodul kommer snart...
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Activity Tab -->
|
||
|
|
<div class="tab-pane fade" id="activity">
|
||
|
|
<h5 class="fw-bold mb-4">Aktivitet</h5>
|
||
|
|
<div id="activityContainer">
|
||
|
|
<div class="text-center py-5">
|
||
|
|
<div class="spinner-border text-primary"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{% endblock %}
|
||
|
|
|
||
|
|
{% block extra_js %}
|
||
|
|
<script>
|
||
|
|
const customerId = parseInt(window.location.pathname.split('/').pop());
|
||
|
|
let customerData = null;
|
||
|
|
|
||
|
|
document.addEventListener('DOMContentLoaded', () => {
|
||
|
|
loadCustomer();
|
||
|
|
|
||
|
|
// Load contacts when tab is shown
|
||
|
|
document.querySelector('a[href="#contacts"]').addEventListener('shown.bs.tab', () => {
|
||
|
|
loadContacts();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Load activity when tab is shown
|
||
|
|
document.querySelector('a[href="#activity"]').addEventListener('shown.bs.tab', () => {
|
||
|
|
loadActivity();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
async function loadCustomer() {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/v1/customers/${customerId}`);
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error('Customer not found');
|
||
|
|
}
|
||
|
|
|
||
|
|
customerData = await response.json();
|
||
|
|
displayCustomer(customerData);
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Failed to load customer:', error);
|
||
|
|
alert('Kunne ikke indlæse kunde');
|
||
|
|
window.location.href = '/customers';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function displayCustomer(customer) {
|
||
|
|
// Update page title
|
||
|
|
document.title = `${customer.name} - BMC Hub`;
|
||
|
|
|
||
|
|
// Header
|
||
|
|
document.getElementById('customerAvatar').textContent = getInitials(customer.name);
|
||
|
|
document.getElementById('customerName').textContent = customer.name;
|
||
|
|
document.getElementById('customerCity').textContent = customer.city || '';
|
||
|
|
|
||
|
|
const statusBadge = customer.is_active
|
||
|
|
? '<i class="bi bi-check-circle me-1"></i>Aktiv'
|
||
|
|
: '<i class="bi bi-x-circle me-1"></i>Inaktiv';
|
||
|
|
document.getElementById('customerStatus').innerHTML = statusBadge;
|
||
|
|
|
||
|
|
const sourceBadge = customer.vtiger_id
|
||
|
|
? '<i class="bi bi-cloud me-1"></i>vTiger'
|
||
|
|
: '<i class="bi bi-hdd me-1"></i>Lokal';
|
||
|
|
document.getElementById('customerSource').innerHTML = sourceBadge;
|
||
|
|
|
||
|
|
// Company Information
|
||
|
|
document.getElementById('cvrNumber').textContent = customer.cvr_number || '-';
|
||
|
|
document.getElementById('address').textContent = customer.address || '-';
|
||
|
|
document.getElementById('postalCity').textContent = customer.postal_code && customer.city
|
||
|
|
? `${customer.postal_code} ${customer.city}`
|
||
|
|
: customer.city || '-';
|
||
|
|
document.getElementById('email').textContent = customer.email || '-';
|
||
|
|
document.getElementById('phone').textContent = customer.phone || '-';
|
||
|
|
document.getElementById('website').textContent = customer.website || '-';
|
||
|
|
|
||
|
|
// Economic Information
|
||
|
|
document.getElementById('economicNumber').textContent = customer.economic_customer_number || '-';
|
||
|
|
document.getElementById('paymentTerms').textContent = customer.payment_terms_days
|
||
|
|
? `${customer.payment_terms_days} dage netto`
|
||
|
|
: '-';
|
||
|
|
document.getElementById('vatZone').textContent = customer.vat_zone || '-';
|
||
|
|
document.getElementById('currency').textContent = customer.currency_code || 'DKK';
|
||
|
|
document.getElementById('ean').textContent = customer.ean || '-';
|
||
|
|
document.getElementById('barred').innerHTML = customer.barred
|
||
|
|
? '<span class="badge bg-danger">Ja</span>'
|
||
|
|
: '<span class="badge bg-success">Nej</span>';
|
||
|
|
|
||
|
|
// Integration
|
||
|
|
document.getElementById('vtigerId').textContent = customer.vtiger_id || '-';
|
||
|
|
document.getElementById('vtigerSyncAt').textContent = customer.vtiger_last_sync_at
|
||
|
|
? new Date(customer.vtiger_last_sync_at).toLocaleString('da-DK')
|
||
|
|
: '-';
|
||
|
|
document.getElementById('economicSyncAt').textContent = customer.economic_last_sync_at
|
||
|
|
? new Date(customer.economic_last_sync_at).toLocaleString('da-DK')
|
||
|
|
: '-';
|
||
|
|
document.getElementById('createdAt').textContent = new Date(customer.created_at).toLocaleString('da-DK');
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadContacts() {
|
||
|
|
const container = document.getElementById('contactsContainer');
|
||
|
|
container.innerHTML = '<div class="col-12 text-center py-5"><div class="spinner-border text-primary"></div></div>';
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/v1/customers/${customerId}/contacts`);
|
||
|
|
const contacts = await response.json();
|
||
|
|
|
||
|
|
if (!contacts || contacts.length === 0) {
|
||
|
|
container.innerHTML = '<div class="col-12 text-center py-5 text-muted">Ingen kontakter endnu</div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
container.innerHTML = contacts.map(contact => `
|
||
|
|
<div class="col-md-6">
|
||
|
|
<div class="contact-card">
|
||
|
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||
|
|
<div>
|
||
|
|
<h6 class="fw-bold mb-1">${escapeHtml(contact.name)}</h6>
|
||
|
|
<div class="text-muted small">${contact.title || 'Kontakt'}</div>
|
||
|
|
</div>
|
||
|
|
${contact.is_primary ? '<span class="badge bg-primary">Primær</span>' : ''}
|
||
|
|
</div>
|
||
|
|
<div class="d-flex flex-column gap-2">
|
||
|
|
${contact.email ? `
|
||
|
|
<div class="d-flex align-items-center">
|
||
|
|
<i class="bi bi-envelope me-2 text-muted"></i>
|
||
|
|
<a href="mailto:${contact.email}">${contact.email}</a>
|
||
|
|
</div>
|
||
|
|
` : ''}
|
||
|
|
${contact.phone ? `
|
||
|
|
<div class="d-flex align-items-center">
|
||
|
|
<i class="bi bi-telephone me-2 text-muted"></i>
|
||
|
|
<a href="tel:${contact.phone}">${contact.phone}</a>
|
||
|
|
</div>
|
||
|
|
` : ''}
|
||
|
|
${contact.mobile ? `
|
||
|
|
<div class="d-flex align-items-center">
|
||
|
|
<i class="bi bi-phone me-2 text-muted"></i>
|
||
|
|
<a href="tel:${contact.mobile}">${contact.mobile}</a>
|
||
|
|
</div>
|
||
|
|
` : ''}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`).join('');
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Failed to load contacts:', error);
|
||
|
|
container.innerHTML = '<div class="col-12 text-center py-5 text-danger">Kunne ikke indlæse kontakter</div>';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadActivity() {
|
||
|
|
const container = document.getElementById('activityContainer');
|
||
|
|
container.innerHTML = '<div class="text-center py-5"><div class="spinner-border text-primary"></div></div>';
|
||
|
|
|
||
|
|
// TODO: Implement activity log API endpoint
|
||
|
|
setTimeout(() => {
|
||
|
|
container.innerHTML = '<div class="text-muted text-center py-5">Ingen aktivitet at vise</div>';
|
||
|
|
}, 500);
|
||
|
|
}
|
||
|
|
|
||
|
|
function editCustomer() {
|
||
|
|
// TODO: Open edit modal with pre-filled data
|
||
|
|
console.log('Edit customer:', customerId);
|
||
|
|
}
|
||
|
|
|
||
|
|
function showAddContactModal() {
|
||
|
|
// TODO: Open add contact modal
|
||
|
|
console.log('Add contact for customer:', customerId);
|
||
|
|
}
|
||
|
|
|
||
|
|
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 %}
|