- Added a new column `subscriptions_locked` to the `customers` table to manage subscription access. - Implemented a script to create new modules from a template, including updates to various files (module.json, README.md, router.py, views.py, and migration SQL). - Developed a script to import BMC Office subscriptions from an Excel file into the database, including error handling and statistics reporting. - Created a script to lookup and update missing CVR numbers using the CVR.dk API. - Implemented a script to relink Hub customers to e-conomic customer numbers based on name matching. - Developed scripts to sync CVR numbers from Simply-CRM and vTiger to the local customers database.
1401 lines
60 KiB
HTML
1401 lines
60 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;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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 flex-wrap">
|
||
<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>
|
||
<span id="bmcLockedBadge"></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="#subscriptions">
|
||
<i class="bi bi-arrow-repeat"></i>Abonnementer
|
||
</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>
|
||
|
||
<!-- Subscriptions Tab -->
|
||
<div class="tab-pane fade" id="subscriptions">
|
||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||
<h5 class="fw-bold mb-0">Abonnementer & Salgsordre</h5>
|
||
<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>
|
||
</div>
|
||
|
||
<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>
|
||
|
||
<!-- 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>
|
||
|
||
<!-- 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>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
const customerId = parseInt(window.location.pathname.split('/').pop());
|
||
let customerData = null;
|
||
|
||
let eventListenersAdded = false;
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
if (eventListenersAdded) {
|
||
console.log('Event listeners already added, skipping...');
|
||
return;
|
||
}
|
||
|
||
loadCustomer();
|
||
|
||
// Load contacts when tab is shown
|
||
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();
|
||
}, { once: false });
|
||
}
|
||
|
||
// Load activity when tab is shown
|
||
const activityTab = document.querySelector('a[href="#activity"]');
|
||
if (activityTab) {
|
||
activityTab.addEventListener('shown.bs.tab', () => {
|
||
loadActivity();
|
||
}, { once: false });
|
||
}
|
||
|
||
eventListenersAdded = true;
|
||
});
|
||
|
||
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;
|
||
|
||
// 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 = '';
|
||
}
|
||
|
||
// 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>';
|
||
}
|
||
}
|
||
|
||
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;
|
||
|
||
// Store subscriptions for editing
|
||
currentSubscriptions = subscriptions || [];
|
||
|
||
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">';
|
||
|
||
const isLocked = customerData?.subscriptions_locked || false;
|
||
|
||
// 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">
|
||
<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>
|
||
</div>
|
||
${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)}
|
||
</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);
|
||
|
||
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" style="cursor: pointer;" onclick="toggleLineItems('${itemId}')">
|
||
<div class="flex-grow-1">
|
||
<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">
|
||
<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>
|
||
</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>` : ''}
|
||
</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('');
|
||
}
|
||
|
||
function renderSubscriptionsList(subscriptions, isLocked = false) {
|
||
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);
|
||
// 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`;
|
||
|
||
return `
|
||
<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}')">
|
||
<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')}
|
||
${sub.cf_subscription_bmclst === '1' ? '<i class="bi bi-lock-fill text-danger ms-2" title="BMC Låst - Skal faktureres fra BMC"></i>' : ''}
|
||
</div>
|
||
<div class="small text-muted mt-1">
|
||
${sub.subscription_no ? `#${escapeHtml(sub.subscription_no)}` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="text-end ms-3">
|
||
<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>
|
||
</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>` : ''}
|
||
${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;">
|
||
<div class="text-center py-3">
|
||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||
<span class="visually-hidden">Indlæser...</span>
|
||
</div>
|
||
<div class="small text-muted mt-2">Henter produktlinjer...</div>
|
||
</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;
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
async function toggleSubscriptionDetails(subscriptionId, 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-primary';
|
||
if (item) item.classList.add('expanded');
|
||
|
||
// 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>';
|
||
}
|
||
}
|
||
} else {
|
||
linesDiv.style.display = 'none';
|
||
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';
|
||
if (item) item.classList.remove('expanded');
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
</script>
|
||
{% endblock %}
|