2025-12-06 02:22:01 +01:00
{% 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;
}
2025-12-11 23:14:20 +01:00
.subscription-item {
transition: all 0.3s ease;
cursor: pointer;
}
.subscription-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12) !important;
}
.subscription-item.expanded {
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15) !important;
border-color: var(--accent) !important;
}
.subscription-column {
min-height: 200px;
}
.column-header {
position: sticky;
top: 0;
z-index: 10;
background: white;
padding: 1rem;
margin: -1rem -1rem 1rem -1rem;
border-radius: 8px 8px 0 0;
}
.line-item-details {
background: rgba(0, 0, 0, 0.02);
border-radius: 6px;
padding: 0.5rem;
}
.expandable-item {
transition: all 0.2s;
}
.expandable-item:hover {
background: rgba(0, 0, 0, 0.02);
}
.chevron-icon {
transition: transform 0.2s;
}
2025-12-06 02:22:01 +01:00
.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);
}
2026-01-08 18:28:00 +01:00
/* Enhanced Edit Button */
.btn-edit-customer {
background: linear-gradient(135deg, #0f4c75 0%, #1a5f8e 100%);
color: white;
border: none;
padding: 0.75rem 1.75rem;
border-radius: 10px;
font-weight: 600;
letter-spacing: 0.3px;
box-shadow: 0 4px 15px rgba(15, 76, 117, 0.3);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.btn-edit-customer::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.5s;
}
.btn-edit-customer:hover {
background: linear-gradient(135deg, #1a5f8e 0%, #0f4c75 100%);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(15, 76, 117, 0.4);
color: white;
}
.btn-edit-customer:hover::before {
left: 100%;
}
.btn-edit-customer:active {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(15, 76, 117, 0.3);
}
.btn-edit-customer i {
transition: transform 0.3s;
}
.btn-edit-customer:hover i {
transform: rotate(-15deg) scale(1.1);
}
2025-12-06 02:22:01 +01:00
< / 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 >
2025-12-13 12:06:28 +01:00
< div class = "d-flex gap-3 align-items-center flex-wrap" >
2025-12-06 02:22:01 +01:00
< 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 >
2025-12-13 12:06:28 +01:00
< span id = "bmcLockedBadge" > < / span >
2025-12-06 02:22:01 +01:00
< / div >
< / div >
< / div >
< div class = "d-flex gap-2" >
2026-01-08 18:28:00 +01:00
< button class = "btn btn-edit-customer" onclick = "editCustomer()" >
< i class = "bi bi-pencil-square me-2" > < / i > Rediger Kunde
2025-12-06 02:22:01 +01:00
< / 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 >
2026-01-11 19:23:21 +01:00
<!-- Bankruptcy Alert -->
< div id = "bankruptcyAlert" class = "alert alert-danger d-flex align-items-center mb-4 d-none border-0 shadow-sm" role = "alert" style = "background-color: #ffeaea; color: #842029;" >
< i class = "bi bi-shield-exclamation flex-shrink-0 me-3 fs-2 animate__animated animate__pulse animate__infinite" > < / i >
< div class = "flex-grow-1" >
< h5 class = "alert-heading mb-1 fw-bold" > ⚠️ KONKURS ALARM< / h5 >
< div > Der er registreret en aktiv konkurs-notifikation på denne kunde. Stop levering og kontakt bogholderiet.< / div >
< div class = "mt-2 small" >
< strong > Emne:< / strong > < span id = "bankruptcySubject" class = "fst-italic" > < / span > < br >
< strong > Modtaget:< / strong > < span id = "bankruptcyDate" > < / span >
< / div >
< / div >
< div >
< a id = "bankruptcyLink" href = "#" class = "btn btn-sm btn-danger px-3" > Se Email < i class = "bi bi-arrow-right ms-1" > < / i > < / a >
< / div >
< / div >
2026-01-08 18:28:00 +01:00
<!-- Data Consistency Alert -->
< div id = "consistencyAlert" class = "alert alert-warning alert-dismissible fade show d-none mt-4" role = "alert" >
< div class = "d-flex align-items-center" >
< i class = "bi bi-exclamation-triangle-fill fs-4 me-3" > < / i >
< div class = "flex-grow-1" >
< strong > Data Uoverensstemmelser Fundet!< / strong >
< p class = "mb-0" >
Der er < span id = "discrepancyCount" class = "fw-bold" > 0< / span > felter med forskellige værdier mellem BMC Hub, vTiger og e-conomic.
< / p >
< / div >
< button type = "button" class = "btn btn-warning ms-3" onclick = "showConsistencyModal()" >
< i class = "bi bi-search me-2" > < / i > Sammenlign
< / button >
< / div >
< button type = "button" class = "btn-close" data-bs-dismiss = "alert" aria-label = "Close" > < / button >
< / div >
2025-12-06 02:22:01 +01:00
<!-- 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 >
2025-12-11 23:14:20 +01:00
< li class = "nav-item" >
< a class = "nav-link" data-bs-toggle = "tab" href = "#subscriptions" >
2025-12-16 15:36:11 +01:00
< i class = "bi bi-arrow-repeat" > < / i > Abonnnents tjek
2025-12-11 23:14:20 +01:00
< / a >
< / li >
2026-01-25 14:46:00 +01:00
< li class = "nav-item" >
< a class = "nav-link" data-bs-toggle = "tab" href = "#billing-matrix" >
< i class = "bi bi-table" > < / i > Abonnements Matrix
< / a >
< / li >
2025-12-06 02:22:01 +01:00
< 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 >
2026-01-11 19:23:21 +01:00
< li class = "nav-item" >
< a class = "nav-link" data-bs-toggle = "tab" href = "#conversations" >
< i class = "bi bi-mic" > < / i > Samtaler
< / a >
< / li >
2025-12-06 02:22:01 +01:00
< / 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 >
2025-12-11 23:14:20 +01:00
<!-- Subscriptions Tab -->
< div class = "tab-pane fade" id = "subscriptions" >
< div class = "d-flex justify-content-between align-items-center mb-4" >
2025-12-16 15:36:11 +01:00
< h5 class = "fw-bold mb-0" > Abonnnents tjek< / h5 >
2025-12-13 12:06:28 +01:00
< div class = "btn-group" >
< button class = "btn btn-success btn-sm" onclick = "showCreateSubscriptionModal()" >
< i class = "bi bi-plus-circle me-2" > < / i > Opret Abonnement
< / button >
< button class = "btn btn-primary btn-sm" onclick = "loadSubscriptions()" >
< i class = "bi bi-arrow-repeat me-2" > < / i > Opdater fra vTiger
< / button >
< / div >
2025-12-11 23:14:20 +01:00
< / div >
2025-12-16 22:07:20 +01:00
<!-- Internal Comment Box -->
< div class = "card mb-4" style = "border-left: 4px solid var(--accent);" >
< div class = "card-body" >
< h6 class = "fw-bold mb-3" >
< i class = "bi bi-shield-lock me-2" > < / i > Intern Kommentar
< small class = "text-muted fw-normal" > (kun synlig for medarbejdere)< / small >
< / h6 >
< div id = "internalCommentDisplay" class = "mb-3" style = "display: none;" >
< div class = "alert alert-light mb-2" >
< div style = "white-space: pre-wrap;" id = "commentText" > < / div >
< / div >
< div class = "d-flex justify-content-between align-items-center" >
< small class = "text-muted" id = "commentMeta" > < / small >
< button type = "button" class = "btn btn-sm btn-outline-primary" onclick = "editInternalComment()" title = "Rediger kommentar" >
< i class = "bi bi-pencil me-1" > < / i > Rediger
< / button >
< / div >
< / div >
< div id = "internalCommentEdit" >
< textarea class = "form-control mb-2" id = "internalCommentInput" rows = "3"
placeholder="Skriv intern note om kundens abonnementer...">< / textarea >
< div class = "d-flex justify-content-end gap-2" >
< button class = "btn btn-sm btn-primary" onclick = "saveInternalComment()" >
< i class = "bi bi-save me-1" > < / i > Gem Kommentar
< / button >
< / div >
< / div >
< / div >
< / div >
2025-12-11 23:14:20 +01:00
< div id = "subscriptionsContainer" >
< div class = "text-center py-5" >
< div class = "spinner-border text-primary" > < / div >
< p class = "text-muted mt-3" > Henter data fra vTiger...< / p >
< / div >
< / div >
< / div >
2026-01-25 14:46:00 +01:00
<!-- Billing Matrix Tab -->
< div class = "tab-pane fade" id = "billing-matrix" >
< div class = "d-flex justify-content-between align-items-center mb-4" >
< h5 class = "fw-bold mb-0" >
< i class = "bi bi-table me-2" > < / i > Abonnements-matrix
< small class = "text-muted fw-normal" > (fra e-conomic)< / small >
< / h5 >
< button class = "btn btn-sm btn-outline-primary" onclick = "loadBillingMatrix()" title = "Hent fakturaer fra e-conomic" >
< i class = "bi bi-arrow-repeat me-1" > < / i > Opdater
< / button >
< / div >
< div id = "billingMatrixContainer" style = "display: none;" >
< div class = "table-responsive" >
< table class = "table table-sm table-hover mb-0" id = "billingMatrixTable" >
< thead class = "table-light" >
< tr id = "matrixHeaderRow" >
< th style = "min-width: 200px;" > Vare< / th >
<!-- Months will be added dynamically -->
< / tr >
< / thead >
< tbody id = "matrixBodyRows" >
<!-- Rows will be added dynamically -->
< / tbody >
< / table >
< / div >
< / div >
< div id = "billingMatrixLoading" class = "text-center py-5" >
< div class = "spinner-border spinner-border-sm text-primary" > < / div >
< p class = "text-muted mt-2" > Henter fakturamatrix fra e-conomic...< / p >
< / div >
< div id = "billingMatrixEmpty" style = "display: none;" class = "text-center py-5" >
< p class = "text-muted" > Ingen fakturaer fundet for denne kunde< / p >
< / div >
< / div >
2025-12-06 02:22:01 +01:00
<!-- 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 >
2026-01-11 19:23:21 +01:00
<!-- Conversations Tab -->
< div class = "tab-pane fade" id = "conversations" >
< div class = "d-flex justify-content-between align-items-center mb-4" >
< h5 class = "fw-bold mb-0" > Samtaler< / h5 >
<!--
< button class = "btn btn-primary btn-sm" onclick = "showUploadConversationModal()" >
< i class = "bi bi-upload me-2" > < / i > Upload Samtale
< / button >
-->
< / div >
< div class = "input-group mb-4" >
< span class = "input-group-text" > < i class = "bi bi-search" > < / i > < / span >
< input type = "text" class = "form-control" id = "conversationSearch" placeholder = "Søg i transskriberinger..." onkeyup = "filterConversations()" >
< / div >
< div id = "conversationsContainer" >
<!-- Loaded via JS -->
< div class = "text-center py-5" >
< span class = "text-muted" > Henter samtaler...< / span >
< / div >
< / div >
< / div >
2025-12-06 02:22:01 +01:00
< / div >
< / div >
< / div >
2025-12-13 12:06:28 +01:00
2026-01-08 18:28:00 +01:00
<!-- Edit Customer Modal -->
< div class = "modal fade" id = "editCustomerModal" tabindex = "-1" >
< div class = "modal-dialog modal-lg" >
< div class = "modal-content" >
< div class = "modal-header bg-primary text-white" >
< h5 class = "modal-title" >
< i class = "bi bi-pencil-square me-2" > < / i > Rediger Kunde
< / h5 >
< button type = "button" class = "btn-close btn-close-white" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< form id = "editCustomerForm" >
< div class = "row g-3" >
<!-- Basic Info -->
< div class = "col-12" >
< h6 class = "text-muted text-uppercase small fw-bold mb-3" >
< i class = "bi bi-building me-2" > < / i > Grundlæggende Oplysninger
< / h6 >
< / div >
< div class = "col-md-6" >
< label for = "editName" class = "form-label" > Virksomhedsnavn *< / label >
< input type = "text" class = "form-control" id = "editName" required >
< / div >
< div class = "col-md-6" >
< label for = "editCvrNumber" class = "form-label" > CVR-nummer< / label >
< input type = "text" class = "form-control" id = "editCvrNumber" maxlength = "20" >
< / div >
< div class = "col-md-6" >
< label for = "editEmail" class = "form-label" > Email< / label >
< input type = "email" class = "form-control" id = "editEmail" >
< / div >
< div class = "col-md-6" >
< label for = "editInvoiceEmail" class = "form-label" > Faktura Email< / label >
< input type = "email" class = "form-control" id = "editInvoiceEmail" >
< / div >
< div class = "col-md-6" >
< label for = "editPhone" class = "form-label" > Telefon< / label >
< input type = "tel" class = "form-control" id = "editPhone" >
< / div >
< div class = "col-md-6" >
< label for = "editMobilePhone" class = "form-label" > Mobil< / label >
< input type = "tel" class = "form-control" id = "editMobilePhone" >
< / div >
< div class = "col-md-6" >
< label for = "editWebsite" class = "form-label" > Hjemmeside< / label >
< input type = "url" class = "form-control" id = "editWebsite" placeholder = "https://" >
< / div >
< div class = "col-md-6" >
< label for = "editCountry" class = "form-label" > Land< / label >
< select class = "form-select" id = "editCountry" >
< option value = "DK" > Danmark< / option >
< option value = "NO" > Norge< / option >
< option value = "SE" > Sverige< / option >
< option value = "DE" > Tyskland< / option >
< option value = "GB" > Storbritannien< / option >
< / select >
< / div >
<!-- Address -->
< div class = "col-12 mt-4" >
< h6 class = "text-muted text-uppercase small fw-bold mb-3" >
< i class = "bi bi-geo-alt me-2" > < / i > Adresse
< / h6 >
< / div >
< div class = "col-12" >
< label for = "editAddress" class = "form-label" > Adresse< / label >
< input type = "text" class = "form-control" id = "editAddress" >
< / div >
< div class = "col-md-4" >
< label for = "editPostalCode" class = "form-label" > Postnummer< / label >
< input type = "text" class = "form-control" id = "editPostalCode" maxlength = "10" >
< / div >
< div class = "col-md-8" >
< label for = "editCity" class = "form-label" > By< / label >
< input type = "text" class = "form-control" id = "editCity" >
< / div >
<!-- Status -->
< div class = "col-12 mt-4" >
< div class = "form-check form-switch" >
< input class = "form-check-input" type = "checkbox" id = "editIsActive" checked >
< label class = "form-check-label" for = "editIsActive" >
< strong > Aktiv kunde< / strong >
< div class = "small text-muted" > Deaktiver for at skjule kunden fra lister< / div >
< / label >
< / div >
< / div >
< / div >
< / form >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" >
< i class = "bi bi-x-lg me-2" > < / i > Annuller
< / button >
< button type = "button" class = "btn btn-primary" onclick = "saveCustomerEdit()" >
< i class = "bi bi-check-lg me-2" > < / i > Gem Ændringer
< / button >
< / div >
< / div >
< / div >
< / div >
2025-12-13 12:06:28 +01:00
<!-- Subscription Modal -->
< div class = "modal fade" id = "subscriptionModal" tabindex = "-1" >
< div class = "modal-dialog" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" id = "subscriptionModalLabel" > Opret Abonnement< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" > < / button >
< / div >
< div class = "modal-body" >
< form id = "subscriptionForm" >
< input type = "hidden" id = "subscriptionId" >
< div class = "mb-3" >
< label for = "subjectInput" class = "form-label" > Emne/Navn *< / label >
< input type = "text" class = "form-control" id = "subjectInput" required >
< / div >
< div class = "row" >
< div class = "col-md-6 mb-3" >
< label for = "startdateInput" class = "form-label" > Startdato *< / label >
< input type = "date" class = "form-control" id = "startdateInput" required >
< / div >
< div class = "col-md-6 mb-3" >
< label for = "enddateInput" class = "form-label" > Slutdato< / label >
< input type = "date" class = "form-control" id = "enddateInput" >
< / div >
< / div >
< div class = "mb-3" >
< label for = "frequencyInput" class = "form-label" > Frekvens *< / label >
< select class = "form-select" id = "frequencyInput" required >
< option value = "Monthly" > Månedlig< / option >
< option value = "Quarterly" > Kvartalsvis< / option >
< option value = "Half-Yearly" > Halvårlig< / option >
< option value = "Yearly" > Årlig< / option >
< / select >
< / div >
< div class = "mb-3" >
< label for = "statusInput" class = "form-label" > Status *< / label >
< select class = "form-select" id = "statusInput" required >
< option value = "Active" > Aktiv< / option >
< option value = "Stopped" > Stoppet< / option >
< option value = "Cancelled" > Annulleret< / option >
< / select >
< / div >
< div class = "alert alert-info" >
< i class = "bi bi-info-circle me-2" > < / i >
Produkter skal tilføjes i vTiger efter oprettelse
< / 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 = "saveSubscription()" > Gem< / button >
< / div >
< / div >
< / div >
< / div >
2025-12-06 02:22:01 +01:00
{% endblock %}
{% block extra_js %}
< script >
const customerId = parseInt(window.location.pathname.split('/').pop());
let customerData = null;
2025-12-11 23:14:20 +01:00
let eventListenersAdded = false;
2025-12-06 02:22:01 +01:00
document.addEventListener('DOMContentLoaded', () => {
2025-12-11 23:14:20 +01:00
if (eventListenersAdded) {
console.log('Event listeners already added, skipping...');
return;
}
2025-12-06 02:22:01 +01:00
loadCustomer();
// Load contacts when tab is shown
2025-12-11 23:14:20 +01:00
const contactsTab = document.querySelector('a[href="#contacts"]');
if (contactsTab) {
contactsTab.addEventListener('shown.bs.tab', () => {
loadContacts();
}, { once: false });
}
// Load subscriptions when tab is shown
const subscriptionsTab = document.querySelector('a[href="#subscriptions"]');
if (subscriptionsTab) {
subscriptionsTab.addEventListener('shown.bs.tab', () => {
loadSubscriptions();
2025-12-16 22:07:20 +01:00
loadInternalComment();
2025-12-11 23:14:20 +01:00
}, { once: false });
}
2025-12-06 02:22:01 +01:00
// Load activity when tab is shown
2025-12-11 23:14:20 +01:00
const activityTab = document.querySelector('a[href="#activity"]');
if (activityTab) {
activityTab.addEventListener('shown.bs.tab', () => {
loadActivity();
}, { once: false });
}
2026-01-11 19:23:21 +01:00
// Load conversations when tab is shown
const conversationsTab = document.querySelector('a[href="#conversations"]');
if (conversationsTab) {
conversationsTab.addEventListener('shown.bs.tab', () => {
loadConversations();
}, { once: false });
}
2025-12-11 23:14:20 +01:00
eventListenersAdded = true;
2025-12-06 02:22:01 +01:00
});
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);
2026-01-08 18:28:00 +01:00
// Check data consistency
await checkDataConsistency();
2025-12-06 02:22:01 +01:00
} 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`;
2026-01-11 19:23:21 +01:00
// Bankruptcy Alert
const bankruptcyAlert = document.getElementById('bankruptcyAlert');
if (customer.bankruptcy_alert) {
document.getElementById('bankruptcySubject').textContent = customer.bankruptcy_alert.subject;
document.getElementById('bankruptcyDate').textContent = new Date(customer.bankruptcy_alert.received_date).toLocaleString('da-DK');
document.getElementById('bankruptcyLink').href = `/emails?id=${customer.bankruptcy_alert.id}`;
bankruptcyAlert.classList.remove('d-none');
// Also add a badge to the header
const extraBadge = document.createElement('span');
extraBadge.className = 'badge bg-danger animate__animated animate__pulse animate__infinite ms-2';
extraBadge.innerHTML = '< i class = "bi bi-shield-exclamation me-1" > < / i > KONKURS';
document.getElementById('customerStatus').parentNode.appendChild(extraBadge);
} else {
bankruptcyAlert.classList.add('d-none');
}
2025-12-06 02:22:01 +01:00
// 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;
2025-12-13 12:06:28 +01:00
// BMC Låst badge
const bmcLockedBadge = document.getElementById('bmcLockedBadge');
if (customer.bmc_locked) {
bmcLockedBadge.innerHTML = `
< span class = "badge bg-danger bg-opacity-90 px-3 py-2" style = "font-size: 0.9rem;" title = "Dette firma skal faktureres fra BMC" >
< i class = "bi bi-lock-fill me-2" > < / i >
< strong > BMC LÅST< / strong >
< / span >
`;
} else {
bmcLockedBadge.innerHTML = '';
}
2025-12-06 02:22:01 +01:00
// 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 > ';
}
}
2025-12-11 23:14:20 +01:00
let subscriptionsLoaded = false;
async function loadSubscriptions() {
const container = document.getElementById('subscriptionsContainer');
// Prevent duplicate loads
if (subscriptionsLoaded & & container.innerHTML.includes('row g-4')) {
console.log('Subscriptions already loaded, skipping...');
return;
}
container.innerHTML = '< div class = "text-center py-5" > < div class = "spinner-border text-primary" > < / div > < p class = "text-muted mt-3" > Henter data fra vTiger...< / p > < / div > ';
try {
const response = await fetch(`/api/v1/customers/${customerId}/subscriptions`);
const data = await response.json();
console.log('Loaded subscriptions:', data);
if (!response.ok) {
throw new Error(data.detail || 'Failed to load subscriptions');
}
if (data.status === 'no_vtiger_link') {
container.innerHTML = `
< div class = "alert alert-warning" >
< i class = "bi bi-exclamation-triangle me-2" > < / i >
${data.message}
< / div >
`;
return;
}
displaySubscriptions(data);
subscriptionsLoaded = true;
} catch (error) {
console.error('Failed to load subscriptions:', error);
container.innerHTML = '< div class = "alert alert-danger" > Kunne ikke indlæse abonnementer< / div > ';
}
}
function displaySubscriptions(data) {
const container = document.getElementById('subscriptionsContainer');
const { recurring_orders, sales_orders, subscriptions, expired_subscriptions, bmc_office_subscriptions } = data;
2025-12-13 12:06:28 +01:00
// Store subscriptions for editing
currentSubscriptions = subscriptions || [];
2025-12-11 23:14:20 +01:00
const totalItems = (sales_orders?.length || 0) + (subscriptions?.length || 0) + (bmc_office_subscriptions?.length || 0);
if (totalItems === 0) {
container.innerHTML = '< div class = "text-center text-muted py-5" > Ingen abonnementer eller salgsordre fundet< / div > ';
return;
}
// Create 3-column layout
let html = '< div class = "row g-3" > ';
2025-12-13 12:06:28 +01:00
const isLocked = customerData?.subscriptions_locked || false;
2025-12-11 23:14:20 +01:00
// Column 1: vTiger Subscriptions
html += `
< div class = "col-lg-4" >
< div class = "subscription-column" >
< div class = "column-header bg-primary bg-opacity-10 border-start border-primary border-4 p-3 mb-3 rounded" >
2025-12-13 12:06:28 +01:00
< div class = "d-flex justify-content-between align-items-start" >
< div >
< h5 class = "fw-bold mb-1" >
< i class = "bi bi-arrow-repeat text-primary me-2" > < / i >
vTiger Abonnementer
${isLocked ? '< i class = "bi bi-lock-fill text-danger ms-2" title = "Abonnementer låst" > < / i > ' : ''}
< / h5 >
< small class = "text-muted" > Fra vTiger Cloud< / small >
< / div >
< button class = "btn btn-sm ${isLocked ? 'btn-danger' : 'btn-outline-secondary'}"
onclick="toggleSubscriptionsLock()"
title="${isLocked ? 'Lås op' : 'Lås abonnementer'}">
< i class = "bi bi-${isLocked ? 'unlock' : 'lock'}-fill" > < / i >
${isLocked ? 'Låst' : 'Lås'}
< / button >
< / div >
2025-12-11 23:14:20 +01:00
< / div >
2025-12-13 12:06:28 +01:00
${isLocked ? '< div class = "alert alert-warning small mb-3" > < i class = "bi bi-lock-fill me-2" > < / i > Abonnementer er låst - kan kun redigeres i vTiger< / div > ' : ''}
${renderSubscriptionsList(subscriptions || [], isLocked)}
2025-12-11 23:14:20 +01:00
< / div >
< / div >
`;
// Column 2: Sales Orders
html += `
< div class = "col-lg-4" >
< div class = "subscription-column" >
< div class = "column-header bg-success bg-opacity-10 border-start border-success border-4 p-3 mb-3 rounded" >
< h5 class = "fw-bold mb-1" >
< i class = "bi bi-cart-check text-success me-2" > < / i >
Åbne Salgsordre
< / h5 >
< small class = "text-muted" > Fra Simply-CRM< / small >
< / div >
${renderSalesOrdersList(sales_orders || [])}
< / div >
< / div >
`;
// Column 3: BMC Office Subscriptions
html += `
< div class = "col-lg-4" >
< div class = "subscription-column" >
< div class = "column-header bg-info bg-opacity-10 border-start border-info border-4 p-3 mb-3 rounded" >
< h5 class = "fw-bold mb-1" >
< i class = "bi bi-database text-info me-2" > < / i >
BMC Office Abonnementer
< / h5 >
< small class = "text-muted" > Fra lokalt system< / small >
< / div >
${renderBmcOfficeSubscriptionsList(bmc_office_subscriptions || [])}
< / div >
< / div >
`;
html += '< / div > ';
// Add comparison stats at bottom
const subTotal = subscriptions?.reduce((sum, sub) => sum + parseFloat(sub.hdnGrandTotal || 0), 0) || 0;
const orderTotal = sales_orders?.reduce((sum, order) => sum + parseFloat(order.hdnGrandTotal || 0), 0) || 0;
const bmcOfficeTotal = bmc_office_subscriptions?.reduce((sum, sub) => sum + parseFloat(sub.total_inkl_moms || 0), 0) || 0;
if (totalItems > 0) {
html += `
< div class = "row g-4 mt-2" >
< div class = "col-12" >
< div class = "info-card bg-light" >
< h6 class = "fw-bold mb-3" > Sammenligning< / h6 >
< div class = "row text-center" >
< div class = "col-md-3" >
< div class = "stat-item" >
< div class = "text-muted small" > vTiger Abonnementer< / div >
< div class = "fs-4 fw-bold text-primary" > ${subscriptions?.length || 0}< / div >
< div class = "text-muted small" > ${subTotal.toFixed(2)} DKK< / div >
< / div >
< / div >
< div class = "col-md-3" >
< div class = "stat-item" >
< div class = "text-muted small" > Åbne Salgsordre< / div >
< div class = "fs-4 fw-bold text-success" > ${sales_orders?.length || 0}< / div >
< div class = "text-muted small" > ${orderTotal.toFixed(2)} DKK< / div >
< / div >
< / div >
< div class = "col-md-3" >
< div class = "stat-item" >
< div class = "text-muted small" > BMC Office Abonnementer< / div >
< div class = "fs-4 fw-bold text-info" > ${bmc_office_subscriptions?.length || 0}< / div >
< div class = "text-muted small" > ${bmcOfficeTotal.toFixed(2)} DKK< / div >
< / div >
< / div >
< div class = "col-md-3" >
< div class = "stat-item" >
< div class = "text-muted small" > Total Værdi< / div >
< div class = "fs-4 fw-bold text-dark" > ${(subTotal + orderTotal + bmcOfficeTotal).toFixed(2)} DKK< / div >
< div class = "text-muted small" > Samlet omsætning< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
`;
}
container.innerHTML = html;
}
function renderRecurringOrdersList(orders) {
if (!orders || orders.length === 0) {
return '< p class = "text-muted small" > Ingen tilbagevendende ordrer< / p > ';
}
return orders.map((order, idx) => {
const itemId = `recurring-${idx}`;
const lineItems = order.lineItems || [];
const hasLineItems = Array.isArray(lineItems) & & lineItems.length > 0;
return `
< div class = "border-bottom pb-3 mb-3" >
< div class = "d-flex justify-content-between align-items-start mb-2" style = "cursor: pointer;" onclick = "toggleLineItems('${itemId}')" >
< div class = "fw-bold" >
< i class = "bi bi-chevron-right me-1" id = "${itemId}-icon" > < / i >
${escapeHtml(order.subject || order.salesorder_no || 'Unnamed')}
< / div >
< span class = "badge bg-${getStatusColor(order.sostatus)}" > ${escapeHtml(order.sostatus || 'Open')}< / span >
< / div >
${order.description ? `< p class = "text-muted small mb-2" > ${escapeHtml(order.description).substring(0, 100)}...< / p > ` : ''}
< div class = "d-flex gap-3 small text-muted flex-wrap" >
${order.recurring_frequency ? `< span > < i class = "bi bi-arrow-repeat me-1" > < / i > ${escapeHtml(order.recurring_frequency)}< / span > ` : ''}
${order.start_period ? `< span > < i class = "bi bi-calendar-event me-1" > < / i > ${formatDate(order.start_period)}< / span > ` : ''}
${order.end_period ? `< span > < i class = "bi bi-calendar-x me-1" > < / i > ${formatDate(order.end_period)}< / span > ` : ''}
${order.hdnGrandTotal ? `< span > < i class = "bi bi-currency-dollar me-1" > < / i > ${parseFloat(order.hdnGrandTotal).toFixed(2)} DKK< / span > ` : ''}
< / div >
${hasLineItems ? `
< div id = "${itemId}-lines" class = "mt-3 ps-3" style = "display: none;" >
< div class = "small" >
< strong > Produktlinjer:< / strong >
${lineItems.map(line => `
< div class = "border-start border-2 border-primary ps-2 py-1 mt-2" >
< div class = "fw-bold" > ${escapeHtml(line.product_name || line.productid)}< / div >
< div class = "text-muted" >
Antal: ${line.quantity || 0} × ${parseFloat(line.listprice || 0).toFixed(2)} DKK =
< strong > ${parseFloat(line.netprice || 0).toFixed(2)} DKK< / strong >
< / div >
${line.comment ? `< div class = "text-muted small" > ${escapeHtml(line.comment)}< / div > ` : ''}
< / div >
`).join('')}
< / div >
< / div >
` : ''}
< / div >
`;
}).join('');
}
function renderSalesOrdersList(orders) {
if (!orders || orders.length === 0) {
return '< div class = "text-center text-muted py-4" > < i class = "bi bi-inbox fs-1 d-block mb-2" > < / i > Ingen salgsordre< / div > ';
}
return orders.map((order, idx) => {
const itemId = `salesorder-${idx}`;
const lineItems = order.lineItems || [];
const hasLineItems = Array.isArray(lineItems) & & lineItems.length > 0;
const total = parseFloat(order.hdnGrandTotal || 0);
2025-12-16 15:36:11 +01:00
const recordId = order.id.includes('x') ? order.id.split('x')[1] : order.id;
const simplycrmUrl = `https://bmcnetworks.simply-crm.dk/index.php?module=SalesOrder&view=Detail&record=${recordId}`;
2025-12-11 23:14:20 +01:00
return `
< div class = "subscription-item border rounded p-3 mb-3 bg-white shadow-sm" >
2025-12-16 15:36:11 +01:00
< div class = "d-flex justify-content-between align-items-start mb-2" >
< div class = "flex-grow-1" style = "cursor: pointer;" onclick = "toggleLineItems('${itemId}')" >
2025-12-11 23:14:20 +01:00
< div class = "fw-bold d-flex align-items-center" >
< i class = "bi bi-chevron-right me-2 text-success" id = "${itemId}-icon" style = "font-size: 0.8rem;" > < / i >
${escapeHtml(order.subject || order.salesorder_no || 'Unnamed')}
< / div >
< div class = "small text-muted mt-1" >
${order.salesorder_no ? `#${escapeHtml(order.salesorder_no)}` : ''}
< / div >
< / div >
< div class = "text-end ms-3" >
2025-12-16 15:36:11 +01:00
< div class = "btn-group btn-group-sm mb-2" role = "group" >
< a href = "${simplycrmUrl}" target = "_blank" class = "btn btn-outline-success" title = "Åbn i Simply-CRM" >
< i class = "bi bi-box-arrow-up-right" > < / i > Simply-CRM
< / a >
< / div >
< div >
< div class = "badge bg-${getStatusColor(order.sostatus)} mb-1" > ${escapeHtml(order.sostatus || 'Open')}< / div >
< div class = "fw-bold text-success" > ${total.toFixed(2)} DKK< / div >
< / div >
2025-12-11 23:14:20 +01:00
< / div >
< / div >
< div class = "d-flex gap-2 flex-wrap small text-muted mt-2" >
${order.recurring_frequency ? `< span class = "badge bg-light text-dark" > < i class = "bi bi-arrow-repeat me-1" > < / i > ${escapeHtml(order.recurring_frequency)}< / span > ` : ''}
2025-12-16 15:36:11 +01:00
${order.last_recurring_date ? `< span class = "badge bg-info text-dark" > < i class = "bi bi-calendar-check me-1" > < / i > Sidste: ${formatDate(order.last_recurring_date)}< / span > ` : ''}
${order.start_period & & order.end_period ? `< span class = "badge bg-light text-dark" > < i class = "bi bi-calendar-range me-1" > < / i > ${formatDate(order.start_period)} - ${formatDate(order.end_period)}< / span > ` : ''}
${order.start_period & & !order.end_period ? `< span class = "badge bg-light text-dark" > < i class = "bi bi-calendar-check me-1" > < / i > Start: ${formatDate(order.start_period)}< / span > ` : ''}
2025-12-11 23:14:20 +01:00
< / div >
${hasLineItems ? `
< div id = "${itemId}-lines" class = "mt-3 pt-3 border-top" style = "display: none;" >
< div class = "small" >
${lineItems.map(line => `
< div class = "d-flex justify-content-between align-items-start py-2 border-bottom" >
< div class = "flex-grow-1" >
< div class = "fw-bold" > ${escapeHtml(line.product_name || line.productid)}< / div >
< div class = "text-muted small" >
${line.quantity || 0} stk × ${parseFloat(line.listprice || 0).toFixed(2)} DKK
< / div >
< / div >
< div class = "text-end fw-bold" >
${parseFloat(line.netprice || 0).toFixed(2)} DKK
< / div >
< / div >
`).join('')}
< div class = "d-flex justify-content-between mt-3 pt-2" >
< span class = "text-muted" > Subtotal:< / span >
< strong > ${parseFloat(order.hdnSubTotal || 0).toFixed(2)} DKK< / strong >
< / div >
< div class = "d-flex justify-content-between text-success fw-bold fs-5" >
< span > Total inkl. moms:< / span >
< strong > ${total.toFixed(2)} DKK< / strong >
< / div >
< / div >
< / div >
` : ''}
< / div >
`;
}).join('');
}
function renderBmcOfficeSubscriptionsList(subscriptions) {
if (!subscriptions || subscriptions.length === 0) {
return '< div class = "text-center text-muted py-4" > < i class = "bi bi-inbox fs-1 d-block mb-2" > < / i > Ingen BMC Office abonnementer< / div > ';
}
return subscriptions.map((sub, idx) => {
const itemId = `bmcoffice-${idx}`;
const total = parseFloat(sub.total_inkl_moms || 0);
const subtotal = parseFloat(sub.subtotal || 0);
const rabat = parseFloat(sub.rabat || 0);
return `
< div class = "subscription-item border rounded p-3 mb-3 bg-white shadow-sm" >
< div class = "d-flex justify-content-between align-items-start mb-2" >
< div class = "flex-grow-1" >
< div class = "fw-bold d-flex align-items-center" >
< i class = "bi bi-box-seam text-info me-2" style = "font-size: 0.8rem;" > < / i >
${escapeHtml(sub.text || 'Unnamed')}
< / div >
< div class = "small text-muted mt-1" >
${sub.firma_name ? `${escapeHtml(sub.firma_name)}` : ''}
< / div >
< / div >
< div class = "text-end ms-3" >
< div class = "badge bg-${sub.active ? 'success' : 'secondary'} mb-1" > ${sub.active ? 'Aktiv' : 'Inaktiv'}< / div >
< div class = "fw-bold text-info" > ${total.toFixed(2)} DKK< / div >
< / div >
< / div >
< div class = "d-flex gap-2 flex-wrap small text-muted mt-2" >
${sub.start_date ? `< span class = "badge bg-light text-dark" > < i class = "bi bi-calendar-check me-1" > < / i > Start: ${formatDate(sub.start_date)}< / span > ` : ''}
${sub.antal ? `< span class = "badge bg-light text-dark" > < i class = "bi bi-stack me-1" > < / i > Antal: ${sub.antal}< / span > ` : ''}
${sub.faktura_firma_name & & sub.faktura_firma_name !== sub.firma_name ? `< span class = "badge bg-light text-dark" > < i class = "bi bi-receipt me-1" > < / i > ${escapeHtml(sub.faktura_firma_name)}< / span > ` : ''}
< / div >
< div class = "mt-3 pt-3 border-top" >
< div class = "small" >
< div class = "d-flex justify-content-between py-1" >
< span class = "text-muted" > Antal:< / span >
< strong > ${sub.antal}< / strong >
< / div >
< div class = "d-flex justify-content-between py-1" >
< span class = "text-muted" > Pris pr. stk:< / span >
< strong > ${parseFloat(sub.pris || 0).toFixed(2)} DKK< / strong >
< / div >
${rabat > 0 ? `
< div class = "d-flex justify-content-between py-1 text-danger" >
< span > Rabat:< / span >
< strong > -${rabat.toFixed(2)} DKK< / strong >
< / div >
` : ''}
${sub.beskrivelse ? `
< div class = "py-1 text-muted small" >
< em > ${escapeHtml(sub.beskrivelse)}< / em >
< / div >
` : ''}
< div class = "d-flex justify-content-between mt-2 pt-2 border-top" >
< span class = "text-muted" > Subtotal:< / span >
< strong > ${subtotal.toFixed(2)} DKK< / strong >
< / div >
< div class = "d-flex justify-content-between text-info fw-bold fs-5" >
< span > Total inkl. moms:< / span >
< strong > ${total.toFixed(2)} DKK< / strong >
< / div >
< / div >
< / div >
< / div >
`;
}).join('');
}
2025-12-13 12:06:28 +01:00
function renderSubscriptionsList(subscriptions, isLocked = false) {
2025-12-11 23:14:20 +01:00
if (!subscriptions || subscriptions.length === 0) {
return '< div class = "text-center text-muted py-4" > < i class = "bi bi-inbox fs-1 d-block mb-2" > < / i > Ingen abonnementer< / div > ';
}
return subscriptions.map((sub, idx) => {
const itemId = `subscription-${idx}`;
const total = parseFloat(sub.hdnGrandTotal || 0);
2025-12-13 12:06:28 +01:00
// Extract numeric record ID from vTiger ID (e.g., "72x29932" -> "29932")
const recordId = sub.id.includes('x') ? sub.id.split('x')[1] : sub.id;
const vtigerUrl = `https://bmcnetworks.od2.vtiger.com/view/detail?module=Subscription&id=${recordId}&viewtype=summary`;
2025-12-11 23:14:20 +01:00
return `
2025-12-13 12:06:28 +01:00
< div class = "subscription-item border rounded p-3 mb-3 bg-white shadow-sm ${sub.cf_subscription_bmclst === '1' ? 'border-danger border-3' : ''}" >
< div class = "d-flex justify-content-between align-items-start mb-2" >
< div class = "flex-grow-1" style = "cursor: pointer;" onclick = "toggleSubscriptionDetails('${sub.id}', '${itemId}')" >
2025-12-11 23:14:20 +01:00
< div class = "fw-bold d-flex align-items-center" >
< i class = "bi bi-chevron-right me-2 text-primary" id = "${itemId}-icon" style = "font-size: 0.8rem;" > < / i >
${escapeHtml(sub.subject || sub.subscription_no || 'Unnamed')}
2025-12-13 12:06:28 +01:00
${sub.cf_subscription_bmclst === '1' ? '< i class = "bi bi-lock-fill text-danger ms-2" title = "BMC Låst - Skal faktureres fra BMC" > < / i > ' : ''}
2025-12-11 23:14:20 +01:00
< / div >
< div class = "small text-muted mt-1" >
${sub.subscription_no ? `#${escapeHtml(sub.subscription_no)}` : ''}
< / div >
< / div >
< div class = "text-end ms-3" >
2025-12-13 12:06:28 +01:00
< div class = "btn-group btn-group-sm mb-2" role = "group" >
< a href = "${vtigerUrl}" target = "_blank" class = "btn btn-outline-success" title = "Åbn i vTiger (for at ændre priser)" >
< i class = "bi bi-box-arrow-up-right" > < / i > vTiger
< / a >
${!isLocked ? `
< button class = "btn btn-outline-danger" onclick = "deleteSubscription('${sub.id}', event)" title = "Slet" >
< i class = "bi bi-trash" > < / i >
< / button >
` : ''}
< / div >
< div >
${sub.cf_subscription_bmclst === '1' ? '< div class = "badge bg-danger mb-1" > < i class = "bi bi-lock-fill me-1" > < / i > BMC LÅST< / div > ' : ''}
< div class = "badge bg-${getStatusColor(sub.subscriptionstatus)} mb-1" > ${escapeHtml(sub.subscriptionstatus || 'Active')}< / div >
< div class = "fw-bold text-primary" > ${total.toFixed(2)} DKK< / div >
< / div >
2025-12-11 23:14:20 +01:00
< / div >
< / div >
< div class = "d-flex gap-2 flex-wrap small text-muted mt-2" >
${sub.generateinvoiceevery ? `< span class = "badge bg-light text-dark" > < i class = "bi bi-arrow-repeat me-1" > < / i > ${escapeHtml(sub.generateinvoiceevery)}< / span > ` : ''}
2025-12-15 12:28:12 +01:00
${sub.next_subscription_date ? `< span class = "badge bg-warning text-dark" > < i class = "bi bi-calendar-event me-1" > < / i > Next: ${formatDate(sub.next_subscription_date)}< / span > ` : ''}
2025-12-11 23:14:20 +01:00
${sub.startdate & & sub.enddate ? `< span class = "badge bg-light text-dark" > < i class = "bi bi-calendar-range me-1" > < / i > ${formatDate(sub.startdate)} - ${formatDate(sub.enddate)}< / span > ` : ''}
${sub.startdate ? `< span class = "badge bg-light text-dark" > < i class = "bi bi-calendar-check me-1" > < / i > Start: ${formatDate(sub.startdate)}< / span > ` : ''}
< / div >
< div id = "${itemId}-lines" class = "mt-3 pt-3 border-top" style = "display: none;" >
2025-12-13 12:06:28 +01:00
< div class = "text-center py-3" >
< div class = "spinner-border spinner-border-sm text-primary" role = "status" >
< span class = "visually-hidden" > Indlæser...< / span >
2025-12-11 23:14:20 +01:00
< / div >
2025-12-13 12:06:28 +01:00
< div class = "small text-muted mt-2" > Henter produktlinjer...< / div >
2025-12-11 23:14:20 +01:00
< / div >
< / div >
< / div >
`;
}).join('');
}
function renderInvoicesList(invoices) {
if (!invoices || invoices.length === 0) {
return '< p class = "text-muted small" > Ingen fakturaer< / p > ';
}
return invoices.map((inv, idx) => {
const itemId = `regular-invoice-${idx}`;
const lineItems = inv.lineItems || [];
const hasLineItems = Array.isArray(lineItems) & & lineItems.length > 0;
return `
< div class = "border-bottom pb-3 mb-3" >
< div class = "d-flex justify-content-between align-items-start mb-2" style = "cursor: pointer;" onclick = "toggleLineItems('${itemId}')" >
< div class = "fw-bold" >
< i class = "bi bi-chevron-right me-1" id = "${itemId}-icon" > < / i >
${escapeHtml(inv.subject || inv.invoice_no || 'Unnamed')}
< / div >
< span class = "badge bg-${getStatusColor(inv.invoicestatus)}" > ${escapeHtml(inv.invoicestatus || 'Draft')}< / span >
< / div >
< div class = "d-flex gap-3 small text-muted flex-wrap" >
${inv.invoicedate ? `< span > < i class = "bi bi-calendar me-1" > < / i > ${formatDate(inv.invoicedate)}< / span > ` : ''}
${inv.invoice_no ? `< span > < i class = "bi bi-hash me-1" > < / i > ${escapeHtml(inv.invoice_no)}< / span > ` : ''}
${inv.hdnGrandTotal ? `< span > < i class = "bi bi-currency-dollar me-1" > < / i > ${parseFloat(inv.hdnGrandTotal).toFixed(2)} DKK< / span > ` : ''}
< / div >
${hasLineItems ? `
< div id = "${itemId}-lines" class = "mt-3 ps-3" style = "display: none;" >
< div class = "small" >
< strong > Produktlinjer:< / strong >
${lineItems.map(line => `
< div class = "border-start border-2 border-secondary ps-2 py-1 mt-2" >
< div class = "fw-bold" > ${escapeHtml(line.product_name || line.productid)}< / div >
< div class = "text-muted" >
Antal: ${line.quantity || 0} × ${parseFloat(line.listprice || 0).toFixed(2)} DKK =
< strong > ${parseFloat(line.netprice || 0).toFixed(2)} DKK< / strong >
< / div >
${line.comment ? `< div class = "text-muted small" > ${escapeHtml(line.comment)}< / div > ` : ''}
< / div >
`).join('')}
< div class = "border-top mt-2 pt-2" >
< div class = "d-flex justify-content-between" >
< span > Subtotal:< / span >
< strong > ${parseFloat(inv.hdnSubTotal || 0).toFixed(2)} DKK< / strong >
< / div >
< div class = "d-flex justify-content-between text-info fw-bold" >
< span > Total inkl. moms:< / span >
< strong > ${parseFloat(inv.hdnGrandTotal || 0).toFixed(2)} DKK< / strong >
< / div >
< / div >
< / div >
< / div >
` : ''}
< / div >
`;
}).join('');
}
function getStatusColor(status) {
if (!status) return 'secondary';
const s = status.toLowerCase();
if (s.includes('active') || s.includes('approved')) return 'success';
if (s.includes('pending')) return 'warning';
if (s.includes('cancelled') || s.includes('expired')) return 'danger';
return 'info';
}
function formatDate(dateStr) {
if (!dateStr) return '';
try {
const date = new Date(dateStr);
return date.toLocaleDateString('da-DK', { day: '2-digit', month: '2-digit', year: 'numeric' });
} catch {
return dateStr;
}
}
2025-12-06 02:22:01 +01:00
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);
}
2026-01-11 19:23:21 +01:00
async function loadConversations() {
const container = document.getElementById('conversationsContainer');
container.innerHTML = '< div class = "text-center py-5" > < div class = "spinner-border text-primary" > < / div > < / div > ';
try {
const response = await fetch(`/api/v1/conversations?customer_id=${customerId}`);
if (!response.ok) throw new Error('Failed to load conversations');
const conversations = await response.json();
if (conversations.length === 0) {
container.innerHTML = '< div class = "text-muted text-center py-5" > Ingen samtaler fundet< / div > ';
return;
}
container.innerHTML = conversations.map(c => renderConversationCard(c)).join('');
} catch (error) {
console.error('Error loading conversations:', error);
container.innerHTML = '< div class = "alert alert-danger" > Kunne ikke hente samtaler< / div > ';
}
}
function renderConversationCard(c) {
const date = new Date(c.created_at).toLocaleString();
const duration = c.duration_seconds ? `${Math.floor(c.duration_seconds/60)}:${(c.duration_seconds%60).toString().padStart(2,'0')}` : '';
return `
< div class = "card mb-3 shadow-sm conversation-item ${c.is_private ? 'border-warning' : ''}" data-text = "${(c.transcript || '') + ' ' + (c.title || '')}" >
< div class = "card-body" >
< div class = "d-flex justify-content-between align-items-start mb-2" >
< div >
< h6 class = "card-title fw-bold mb-1" >
${c.is_private ? '< i class = "bi bi-lock-fill text-warning" title = "Privat" > < / i > ' : ''}
${c.title}
< / h6 >
< div class = "small text-muted" >
< i class = "bi bi-calendar me-1" > < / i > ${date}
${duration ? `• < i class = "bi bi-clock me-1" > < / i > ${duration}` : ''}
• < span class = "badge bg-light text-dark border" > ${c.source}< / span >
• < span class = "badge bg-secondary" > ${c.category || 'General'}< / span >
< / div >
< / div >
< div class = "dropdown" >
< button class = "btn btn-link text-muted p-0" data-bs-toggle = "dropdown" >
< i class = "bi bi-three-dots-vertical" > < / i >
< / button >
< ul class = "dropdown-menu dropdown-menu-end" >
< li > < a class = "dropdown-item" href = "#" onclick = "togglePrivacy(${c.id}, ${!c.is_private})" >
${c.is_private ? 'Gør Offentlig' : 'Gør Privat'}
< / a > < / li >
< li > < hr class = "dropdown-divider" > < / li >
< li > < a class = "dropdown-item text-danger" href = "#" onclick = "deleteConversation(${c.id})" > Slet< / a > < / li >
< / ul >
< / div >
< / div >
< audio controls class = "w-100 mb-3 bg-light rounded" >
< source src = "/api/v1/conversations/${c.id}/audio" type = "audio/mpeg" >
Your browser does not support the audio element.
< / audio >
${c.transcript ? `
< div class = "accordion" id = "accordion-${c.id}" >
< div class = "accordion-item border-0" >
< h2 class = "accordion-header" >
< button class = "accordion-button collapsed py-2 bg-light rounded" type = "button" data-bs-toggle = "collapse" data-bs-target = "#collapse-${c.id}" >
< i class = "bi bi-file-text me-2" > < / i > Vis Transskribering
< / button >
< / h2 >
< div id = "collapse-${c.id}" class = "accordion-collapse collapse" data-bs-parent = "#accordion-${c.id}" >
< div class = "accordion-body bg-light rounded mt-2 small font-monospace" style = "white-space: pre-wrap;" > ${c.transcript}< / div >
< / div >
< / div >
< / div >
` : ''}
< / div >
< / div >
`;
}
function filterConversations() {
const query = document.getElementById('conversationSearch').value.toLowerCase();
const items = document.querySelectorAll('.conversation-item');
items.forEach(item => {
const text = item.getAttribute('data-text').toLowerCase();
item.style.display = text.includes(query) ? 'block' : 'none';
});
}
async function togglePrivacy(id, makePrivate) {
try {
await fetch(`/api/v1/conversations/${id}`, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({is_private: makePrivate})
});
loadConversations(); // Reload
} catch(e) { alert('Fejl'); }
}
async function deleteConversation(id) {
if(!confirm('Vil du slette denne samtale?')) return;
// User requested "SLET alt" capability.
// If user confirms again, we do hard delete.
const hard = confirm('ADVARSEL: Skal dette være en permanent sletning af fil og data? (Kan ikke fortrydes)\n\nTryk OK for Permanent Sletning.\nTryk Cancel for Papirkurv.');
try {
await fetch(`/api/v1/conversations/${id}?hard_delete=${hard}`, { method: 'DELETE' });
loadConversations();
} catch(e) { alert('Fejl under sletning'); }
}
2025-12-13 12:06:28 +01:00
async function toggleSubscriptionDetails(subscriptionId, itemId) {
2025-12-11 23:14:20 +01:00
const linesDiv = document.getElementById(`${itemId}-lines`);
const icon = document.getElementById(`${itemId}-icon`);
if (!linesDiv) return;
const item = linesDiv.closest('.subscription-item');
if (linesDiv.style.display === 'none') {
linesDiv.style.display = 'block';
if (icon) icon.className = 'bi bi-chevron-down me-2 text-primary';
if (item) item.classList.add('expanded');
2025-12-13 12:06:28 +01:00
// Fetch line items if not already loaded
if (linesDiv.querySelector('.spinner-border')) {
try {
const response = await fetch(`/api/v1/subscriptions/${subscriptionId}`);
const data = await response.json();
if (data.status === 'success') {
const sub = data.subscription;
const lineItems = sub.LineItems || [];
const total = parseFloat(sub.hdnGrandTotal || 0);
if (lineItems.length > 0) {
linesDiv.innerHTML = `
< div class = "small" >
< div class = "d-flex justify-content-between align-items-center mb-2" >
< strong > Produktlinjer:< / strong >
< span class = "text-muted small" >
< i class = "bi bi-info-circle me-1" > < / i >
For at ændre priser, klik "Åbn i vTiger"
< / span >
< / div >
${lineItems.map(line => `
< div class = "d-flex justify-content-between align-items-start py-2 border-bottom" >
< div class = "flex-grow-1" >
< div class = "fw-bold" > ${escapeHtml(line.product_name || line.productid)}< / div >
< div class = "text-muted small" >
${line.quantity} stk × ${parseFloat(line.listprice).toFixed(2)} DKK
< / div >
${line.comment ? `< div class = "text-muted small" > < i class = "bi bi-chat-left-text me-1" > < / i > ${escapeHtml(line.comment)}< / div > ` : ''}
< / div >
< div class = "text-end fw-bold" >
${parseFloat(line.netprice || 0).toFixed(2)} DKK
< / div >
< / div >
`).join('')}
< div class = "d-flex justify-content-between mt-3 pt-2" >
< span class = "text-muted" > Subtotal:< / span >
< strong > ${parseFloat(sub.hdnSubTotal || 0).toFixed(2)} DKK< / strong >
< / div >
< div class = "d-flex justify-content-between text-primary fw-bold fs-5" >
< span > Total inkl. moms:< / span >
< strong > ${total.toFixed(2)} DKK< / strong >
< / div >
< / div >
`;
} else {
linesDiv.innerHTML = '< div class = "text-muted small" > Ingen produktlinjer< / div > ';
}
} else {
linesDiv.innerHTML = '< div class = "text-danger small" > Kunne ikke hente produktlinjer< / div > ';
}
} catch (error) {
console.error('Error fetching subscription details:', error);
linesDiv.innerHTML = '< div class = "text-danger small" > Fejl ved indlæsning< / div > ';
}
}
2025-12-11 23:14:20 +01:00
} else {
linesDiv.style.display = 'none';
2025-12-13 12:06:28 +01:00
if (icon) icon.className = 'bi bi-chevron-right me-2 text-primary';
if (item) item.classList.remove('expanded');
}
}
function toggleLineItems(itemId) {
const linesDiv = document.getElementById(`${itemId}-lines`);
const icon = document.getElementById(`${itemId}-icon`);
if (!linesDiv) return;
const item = linesDiv.closest('.subscription-item');
if (linesDiv.style.display === 'none') {
linesDiv.style.display = 'block';
if (icon) icon.className = 'bi bi-chevron-down me-2 text-success';
if (item) item.classList.add('expanded');
} else {
linesDiv.style.display = 'none';
if (icon) icon.className = 'bi bi-chevron-right me-2 text-success';
2025-12-11 23:14:20 +01:00
if (item) item.classList.remove('expanded');
}
}
2025-12-06 02:22:01 +01:00
function editCustomer() {
2026-01-08 18:28:00 +01:00
if (!customerData) {
alert('Kunde data ikke indlæst endnu');
return;
}
// Pre-fill form with current data
document.getElementById('editName').value = customerData.name || '';
document.getElementById('editCvrNumber').value = customerData.cvr_number || '';
document.getElementById('editEmail').value = customerData.email || '';
document.getElementById('editInvoiceEmail').value = customerData.invoice_email || '';
document.getElementById('editPhone').value = customerData.phone || '';
document.getElementById('editMobilePhone').value = customerData.mobile_phone || '';
document.getElementById('editWebsite').value = customerData.website || '';
document.getElementById('editCountry').value = customerData.country || 'DK';
document.getElementById('editAddress').value = customerData.address || '';
document.getElementById('editPostalCode').value = customerData.postal_code || '';
document.getElementById('editCity').value = customerData.city || '';
document.getElementById('editIsActive').checked = customerData.is_active !== false;
// Show modal
const modal = new bootstrap.Modal(document.getElementById('editCustomerModal'));
modal.show();
}
async function saveCustomerEdit() {
const updateData = {
name: document.getElementById('editName').value,
cvr_number: document.getElementById('editCvrNumber').value || null,
email: document.getElementById('editEmail').value || null,
invoice_email: document.getElementById('editInvoiceEmail').value || null,
phone: document.getElementById('editPhone').value || null,
mobile_phone: document.getElementById('editMobilePhone').value || null,
website: document.getElementById('editWebsite').value || null,
country: document.getElementById('editCountry').value || 'DK',
address: document.getElementById('editAddress').value || null,
postal_code: document.getElementById('editPostalCode').value || null,
city: document.getElementById('editCity').value || null,
is_active: document.getElementById('editIsActive').checked
};
// Validate required fields
if (!updateData.name || updateData.name.trim() === '') {
alert('Virksomhedsnavn er påkrævet');
return;
}
try {
const response = await fetch(`/api/v1/customers/${customerId}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(updateData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Kunne ikke opdatere kunde');
}
const updatedCustomer = await response.json();
// Close modal
bootstrap.Modal.getInstance(document.getElementById('editCustomerModal')).hide();
// Reload customer data
await loadCustomer();
// Show success message
alert('✓ Kunde opdateret succesfuldt!');
} catch (error) {
console.error('Error updating customer:', error);
alert('Fejl ved opdatering: ' + error.message);
}
}
// Data Consistency Functions
let consistencyData = null;
async function checkDataConsistency() {
try {
const response = await fetch(`/api/v1/customers/${customerId}/data-consistency`);
const data = await response.json();
if (!data.enabled) {
console.log('Data consistency checking is disabled');
return;
}
consistencyData = data;
// Show alert if there are discrepancies
if (data.discrepancy_count > 0) {
document.getElementById('discrepancyCount').textContent = data.discrepancy_count;
document.getElementById('consistencyAlert').classList.remove('d-none');
} else {
document.getElementById('consistencyAlert').classList.add('d-none');
}
} catch (error) {
console.error('Error checking data consistency:', error);
}
}
function showConsistencyModal() {
if (!consistencyData) {
alert('Ingen data tilgængelig');
return;
}
const tbody = document.getElementById('consistencyTableBody');
tbody.innerHTML = '';
// Field labels in Danish
const fieldLabels = {
'name': 'Navn',
'cvr_number': 'CVR Nummer',
'address': 'Adresse',
'city': 'By',
'postal_code': 'Postnummer',
'country': 'Land',
'phone': 'Telefon',
'mobile_phone': 'Mobil',
'email': 'Email',
'website': 'Hjemmeside',
'invoice_email': 'Faktura Email'
};
// Only show fields with discrepancies
for (const [fieldName, fieldData] of Object.entries(consistencyData.discrepancies)) {
if (!fieldData.discrepancy) continue;
const row = document.createElement('tr');
row.className = 'table-warning';
// Field name
const fieldCell = document.createElement('td');
fieldCell.innerHTML = `< strong > ${fieldLabels[fieldName] || fieldName}< / strong > `;
row.appendChild(fieldCell);
// Hub value
const hubCell = document.createElement('td');
hubCell.innerHTML = `
< div class = "form-check" >
< input class = "form-check-input" type = "radio" name = "field_${fieldName}"
id="hub_${fieldName}" value="hub" data-value="${fieldData.hub || ''}">
< label class = "form-check-label" for = "hub_${fieldName}" >
${fieldData.hub || '< em class = "text-muted" > Tom< / em > '}
< / label >
< / div >
`;
row.appendChild(hubCell);
// vTiger value
const vtigerCell = document.createElement('td');
if (consistencyData.systems_available.vtiger) {
vtigerCell.innerHTML = `
< div class = "form-check" >
< input class = "form-check-input" type = "radio" name = "field_${fieldName}"
id="vtiger_${fieldName}" value="vtiger" data-value="${fieldData.vtiger || ''}">
< label class = "form-check-label" for = "vtiger_${fieldName}" >
${fieldData.vtiger || '< em class = "text-muted" > Tom< / em > '}
< / label >
< / div >
`;
} else {
vtigerCell.innerHTML = '< em class = "text-muted" > Ikke tilgængelig< / em > ';
}
row.appendChild(vtigerCell);
// e-conomic value
const economicCell = document.createElement('td');
if (consistencyData.systems_available.economic) {
economicCell.innerHTML = `
< div class = "form-check" >
< input class = "form-check-input" type = "radio" name = "field_${fieldName}"
id="economic_${fieldName}" value="economic" data-value="${fieldData.economic || ''}">
< label class = "form-check-label" for = "economic_${fieldName}" >
${fieldData.economic || '< em class = "text-muted" > Tom< / em > '}
< / label >
< / div >
`;
} else {
economicCell.innerHTML = '< em class = "text-muted" > Ikke tilgængelig< / em > ';
}
row.appendChild(economicCell);
// Action cell (which system to use)
const actionCell = document.createElement('td');
actionCell.innerHTML = '< span class = "text-muted" > ← Vælg< / span > ';
row.appendChild(actionCell);
tbody.appendChild(row);
}
const modal = new bootstrap.Modal(document.getElementById('consistencyModal'));
modal.show();
}
async function syncSelectedFields() {
const selections = [];
// Gather all selected values
const radioButtons = document.querySelectorAll('#consistencyTableBody input[type="radio"]:checked');
if (radioButtons.length === 0) {
alert('Vælg venligst mindst ét felt at synkronisere');
return;
}
radioButtons.forEach(radio => {
const fieldName = radio.name.replace('field_', '');
const sourceSystem = radio.value;
const sourceValue = radio.dataset.value;
selections.push({
field_name: fieldName,
source_system: sourceSystem,
source_value: sourceValue
});
});
// Confirm action
if (!confirm(`Du er ved at synkronisere ${selections.length} felt(er) på tværs af alle systemer. Fortsæt?`)) {
return;
}
// Sync each field
let successCount = 0;
let failCount = 0;
for (const selection of selections) {
try {
const response = await fetch(
`/api/v1/customers/${customerId}/sync-field?` +
new URLSearchParams(selection),
{ method: 'POST' }
);
if (response.ok) {
successCount++;
} else {
failCount++;
console.error(`Failed to sync ${selection.field_name}`);
}
} catch (error) {
failCount++;
console.error(`Error syncing ${selection.field_name}:`, error);
}
}
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('consistencyModal'));
modal.hide();
// Show result
if (failCount === 0) {
alert(`✓ ${successCount} felt(er) synkroniseret succesfuldt!`);
} else {
alert(`⚠️ ${successCount} felt(er) synkroniseret, ${failCount} fejlede`);
}
// Reload customer data and recheck consistency
await loadCustomer();
await checkDataConsistency();
2025-12-06 02:22:01 +01:00
}
function showAddContactModal() {
// TODO: Open add contact modal
console.log('Add contact for customer:', customerId);
}
2025-12-13 12:06:28 +01:00
// Subscription management functions
let currentSubscriptions = [];
function showCreateSubscriptionModal() {
if (!customerData || !customerData.vtiger_id) {
alert('Kunden er ikke linket til vTiger');
return;
}
const modal = new bootstrap.Modal(document.getElementById('subscriptionModal'));
document.getElementById('subscriptionModalLabel').textContent = 'Opret Nyt Abonnement';
document.getElementById('subscriptionForm').reset();
document.getElementById('subscriptionId').value = '';
modal.show();
}
async function editSubscription(subscriptionId, event) {
event.stopPropagation();
// Find subscription data
const sub = currentSubscriptions.find(s => s.id === subscriptionId);
if (!sub) {
alert('Abonnement ikke fundet');
return;
}
// Fill form
document.getElementById('subscriptionId').value = subscriptionId;
document.getElementById('subjectInput').value = sub.subject || '';
document.getElementById('startdateInput').value = sub.startdate || '';
document.getElementById('enddateInput').value = sub.enddate || '';
document.getElementById('frequencyInput').value = sub.generateinvoiceevery || 'Monthly';
document.getElementById('statusInput').value = sub.subscriptionstatus || 'Active';
// Show modal
const modal = new bootstrap.Modal(document.getElementById('subscriptionModal'));
document.getElementById('subscriptionModalLabel').textContent = 'Rediger Abonnement';
modal.show();
}
async function deleteSubscription(subscriptionId, event) {
event.stopPropagation();
if (!confirm('Er du sikker på at du vil slette dette abonnement?')) {
return;
}
try {
const response = await fetch(`/api/v1/subscriptions/${subscriptionId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete subscription');
}
alert('Abonnement slettet');
loadSubscriptions(); // Reload
} catch (error) {
console.error('Error deleting subscription:', error);
alert('Kunne ikke slette abonnement: ' + error.message);
}
}
async function saveSubscription() {
const subscriptionId = document.getElementById('subscriptionId').value;
const isEdit = !!subscriptionId;
const data = {
subject: document.getElementById('subjectInput').value,
startdate: document.getElementById('startdateInput').value,
enddate: document.getElementById('enddateInput').value || null,
generateinvoiceevery: document.getElementById('frequencyInput').value,
subscriptionstatus: document.getElementById('statusInput').value,
products: [] // TODO: Add product picker
};
if (!isEdit) {
data.account_id = customerData.vtiger_id;
}
try {
let response;
if (isEdit) {
response = await fetch(`/api/v1/subscriptions/${subscriptionId}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
} else {
response = await fetch(`/api/v1/customers/${customerId}/subscriptions`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
}
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to save subscription');
}
alert(isEdit ? 'Abonnement opdateret' : 'Abonnement oprettet');
bootstrap.Modal.getInstance(document.getElementById('subscriptionModal')).hide();
loadSubscriptions(); // Reload
} catch (error) {
console.error('Error saving subscription:', error);
alert('Kunne ikke gemme abonnement: ' + error.message);
}
}
2025-12-06 02:22:01 +01:00
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;
}
2025-12-13 12:06:28 +01:00
async function toggleSubscriptionsLock() {
const currentlyLocked = customerData?.subscriptions_locked || false;
const action = currentlyLocked ? 'låse op' : 'låse';
if (!confirm(`Er du sikker på at du vil ${action} abonnementer for denne kunde?\n\n${currentlyLocked ? 'Efter oplåsning kan abonnementer redigeres i BMC Hub.' : 'Efter låsning kan abonnementer kun redigeres direkte i vTiger.'}`)) {
return;
}
try {
const response = await fetch(`/api/v1/customers/${customerId}/subscriptions/lock`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ locked: !currentlyLocked })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Kunne ikke opdatere låsestatus');
}
const result = await response.json();
// Reload customer and subscriptions
await loadCustomer();
await loadSubscriptions();
alert(`✓ ${result.message}\n\n${result.note || ''}`);
} catch (error) {
console.error('Error toggling lock:', error);
alert('Fejl: ' + error.message);
}
}
2025-12-16 22:07:20 +01:00
async function saveInternalComment() {
const commentInput = document.getElementById('internalCommentInput');
const commentText = commentInput.value.trim();
if (!commentText) {
alert('Indtast venligst en kommentar');
return;
}
try {
const response = await fetch(`/api/v1/customers/${customerId}/subscription-comment`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ comment: commentText })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Kunne ikke gemme kommentar');
}
const result = await response.json();
// Clear input
commentInput.value = '';
// Show saved comment
displayInternalComment(result);
alert('✓ Kommentar gemt');
} catch (error) {
console.error('Error saving comment:', error);
alert('Fejl: ' + error.message);
}
}
async function loadInternalComment() {
try {
const response = await fetch(`/api/v1/customers/${customerId}/subscription-comment`);
if (response.status === 404) {
// No comment yet, that's fine
return;
}
if (!response.ok) {
throw new Error('Kunne ikke hente kommentar');
}
const result = await response.json();
displayInternalComment(result);
} catch (error) {
console.error('Error loading comment:', error);
}
}
function displayInternalComment(data) {
const displayDiv = document.getElementById('internalCommentDisplay');
const editDiv = document.getElementById('internalCommentEdit');
const commentMeta = document.getElementById('commentMeta');
const commentText = document.getElementById('commentText');
if (data & & data.comment) {
commentText.textContent = data.comment;
// Format timestamp
const timestamp = new Date(data.created_at).toLocaleString('da-DK', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
commentMeta.textContent = `Oprettet af ${data.created_by || 'System'} • ${timestamp}`;
displayDiv.style.display = 'block';
editDiv.style.display = 'none';
} else {
displayDiv.style.display = 'none';
editDiv.style.display = 'block';
}
}
function editInternalComment() {
const commentText = document.getElementById('commentText').textContent;
const commentInput = document.getElementById('internalCommentInput');
const displayDiv = document.getElementById('internalCommentDisplay');
const editDiv = document.getElementById('internalCommentEdit');
// Show input, populate with existing text
commentInput.value = commentText;
editDiv.style.display = 'block';
displayDiv.style.display = 'none';
}
2026-01-25 14:46:00 +01:00
/**
* Load and render subscription billing matrix from e-conomic
*/
async function loadBillingMatrix() {
const loading = document.getElementById('billingMatrixLoading');
const container = document.getElementById('billingMatrixContainer');
const empty = document.getElementById('billingMatrixEmpty');
// Show loading state
loading.style.display = 'block';
container.style.display = 'none';
empty.style.display = 'none';
try {
const response = await fetch(`/api/v1/customers/${customerId}/subscriptions/billing-matrix`);
const matrix = await response.json();
console.log('📊 Billing matrix:', matrix);
if (!response.ok) {
throw new Error(matrix.detail || 'Failed to load billing matrix');
}
// Check if matrix has products
if (!matrix.products || matrix.products.length === 0) {
empty.style.display = 'block';
loading.style.display = 'none';
return;
}
// Render matrix
renderBillingMatrix(matrix);
} catch (error) {
console.error('Failed to load billing matrix:', error);
loading.innerHTML = `< div class = "alert alert-danger" > < i class = "bi bi-exclamation-circle me-2" > < / i > ${error.message}< / div > `;
}
}
function renderBillingMatrix(matrix) {
const headerRow = document.getElementById('matrixHeaderRow');
const bodyRows = document.getElementById('matrixBodyRows');
// Get all unique months across all products
const monthsSet = new Set();
matrix.products.forEach(product => {
product.rows.forEach(row => {
monthsSet.add(row.year_month);
});
});
const months = Array.from(monthsSet).sort();
if (months.length === 0) {
document.getElementById('billingMatrixEmpty').style.display = 'block';
document.getElementById('billingMatrixLoading').style.display = 'none';
return;
}
// Build header with month columns
headerRow.innerHTML = '< th style = "min-width: 200px;" > Vare< / th > ' +
months.map(month => {
const date = new Date(month + '-01');
const label = date.toLocaleDateString('da-DK', { month: 'short', year: '2-digit' });
return `< th style = "min-width: 100px; text-align: center; font-size: 0.9rem;" > ${label}< / th > `;
}).join('') +
'< th style = "min-width: 80px; text-align: right;" > I alt< / th > ';
// Build body with product rows
bodyRows.innerHTML = matrix.products.map(product => {
const monthCells = months.map(month => {
const cell = product.rows.find(r => r.year_month === month);
if (!cell) {
return '< td class = "text-muted text-center" style = "font-size: 0.85rem;" > -< / td > ';
}
const amount = cell.amount || 0;
const statusBadge = getStatusBadge(cell.status);
const tooltip = cell.period_label ? ` title="${cell.period_label}${cell.invoice_number ? ' • ' + cell.invoice_number : ''}"` : '';
return `< td class = "text-center" style = "font-size: 0.9rem;" $ { tooltip } >
< div class = "d-flex flex-column align-items-center" >
< div class = "fw-500" > ${formatDKK(amount)}< / div >
< div > ${statusBadge}< / div >
< / div >
< / td > `;
}).join('');
return `< tr >
< td class = "fw-500" > ${escapeHtml(product.product_name)}< / td >
${monthCells}
< td class = "text-right fw-bold" style = "text-align: right;" > ${formatDKK(product.total_amount)}< / td >
< / tr > `;
}).join('');
// Show matrix, hide loading
document.getElementById('billingMatrixContainer').style.display = 'block';
document.getElementById('billingMatrixLoading').style.display = 'none';
}
function getStatusBadge(status) {
const badgeMap = {
'paid': { color: 'success', icon: 'check-circle', label: 'Betalt' },
'invoiced': { color: 'warning', icon: 'file-text', label: 'Faktureret' },
'draft': { color: 'secondary', icon: 'file-earmark', label: 'Kladde' },
'missing': { color: 'danger', icon: 'exclamation-triangle', label: 'Manglende' },
'credited': { color: 'info', icon: 'arrow-counterclockwise', label: 'Krediteret' }
};
const badge = badgeMap[status] || { color: 'secondary', icon: 'question-circle', label: status };
return `< span class = "badge bg-${badge.color}" style = "font-size: 0.75rem; margin-top: 2px;" >
< i class = "bi bi-${badge.icon}" style = "font-size: 0.65rem;" > < / i > ${badge.label}
< / span > `;
}
function formatDKK(amount) {
if (!amount || amount === 0) return '0 kr';
return amount.toLocaleString('da-DK', { style: 'currency', currency: 'DKK', minimumFractionDigits: 0 });
}
// Auto-load matrix when subscriptions tab is shown
document.addEventListener('DOMContentLoaded', () => {
const subscriptionsTab = document.querySelector('a[href="#subscriptions"]');
if (subscriptionsTab) {
subscriptionsTab.addEventListener('shown.bs.tab', () => {
// Load matrix if not already loaded
if (!document.getElementById('billingMatrixContainer').innerHTML.includes('table-responsive')) {
setTimeout(() => loadBillingMatrix(), 300);
}
});
}
// Auto-load matrix when billing-matrix tab is shown
const billingMatrixTab = document.querySelector('a[href="#billing-matrix"]');
if (billingMatrixTab) {
billingMatrixTab.addEventListener('shown.bs.tab', () => {
const container = document.getElementById('billingMatrixContainer');
const loading = document.getElementById('billingMatrixLoading');
// Check if we need to load
if (loading.style.display !== 'none' & & container.style.display === 'none') {
loadBillingMatrix();
}
});
}
});
2025-12-06 02:22:01 +01:00
< / script >
2026-01-08 18:28:00 +01:00
<!-- Data Consistency Comparison Modal -->
< div class = "modal fade" id = "consistencyModal" tabindex = "-1" aria-labelledby = "consistencyModalLabel" aria-hidden = "true" >
< div class = "modal-dialog modal-xl" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" id = "consistencyModalLabel" >
< i class = "bi bi-diagram-3 me-2" > < / i > Sammenlign Kundedata
< / h5 >
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" aria-label = "Close" > < / button >
< / div >
< div class = "modal-body" >
< div class = "alert alert-info" >
< i class = "bi bi-info-circle me-2" > < / i >
< strong > Vejledning:< / strong > Vælg den korrekte værdi for hvert felt med uoverensstemmelser.
Når du klikker "Synkroniser Valgte", vil de valgte værdier blive opdateret i alle systemer.
< / div >
< div class = "table-responsive" >
< table class = "table table-hover" >
< thead >
< tr >
< th style = "width: 20%;" > Felt< / th >
< th style = "width: 20%;" > BMC Hub< / th >
< th style = "width: 20%;" > vTiger< / th >
< th style = "width: 20%;" > e-conomic< / th >
< th style = "width: 20%;" > Vælg Korrekt< / th >
< / tr >
< / thead >
< tbody id = "consistencyTableBody" >
<!-- Populated by JavaScript -->
< / tbody >
< / table >
< / div >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" >
< i class = "bi bi-x-circle me-2" > < / i > Luk
< / button >
< button type = "button" class = "btn btn-primary" onclick = "syncSelectedFields()" >
< i class = "bi bi-arrow-repeat me-2" > < / i > Synkroniser Valgte
< / button >
< / div >
< / div >
< / div >
< / div >
2025-12-06 02:22:01 +01:00
{% endblock %}