165 lines
5.8 KiB
HTML
165 lines
5.8 KiB
HTML
|
|
{% extends "shared/frontend/base.html" %}
|
||
|
|
|
||
|
|
{% block title %}Abonnementer - BMC Hub{% endblock %}
|
||
|
|
|
||
|
|
{% block content %}
|
||
|
|
<div class="container-fluid py-4">
|
||
|
|
<div class="row mb-4">
|
||
|
|
<div class="col">
|
||
|
|
<h1 class="h3 mb-0">🔁 Abonnementer</h1>
|
||
|
|
<p class="text-muted">Alle solgte, aktive abonnementer</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="row g-3 mb-4" id="statsCards">
|
||
|
|
<div class="col-md-4">
|
||
|
|
<div class="card border-0 shadow-sm">
|
||
|
|
<div class="card-body">
|
||
|
|
<p class="text-muted small mb-1">Aktive Abonnementer</p>
|
||
|
|
<h3 class="mb-0" id="activeCount">-</h3>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="col-md-4">
|
||
|
|
<div class="card border-0 shadow-sm">
|
||
|
|
<div class="card-body">
|
||
|
|
<p class="text-muted small mb-1">Total Pris (aktive)</p>
|
||
|
|
<h3 class="mb-0" id="totalAmount">-</h3>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="col-md-4">
|
||
|
|
<div class="card border-0 shadow-sm">
|
||
|
|
<div class="card-body">
|
||
|
|
<p class="text-muted small mb-1">Gns. Pris</p>
|
||
|
|
<h3 class="mb-0" id="avgAmount">-</h3>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="card border-0 shadow-sm">
|
||
|
|
<div class="card-header bg-white border-0 py-3">
|
||
|
|
<h5 class="mb-0">Aktive abonnementer</h5>
|
||
|
|
</div>
|
||
|
|
<div class="card-body p-0">
|
||
|
|
<div class="table-responsive">
|
||
|
|
<table class="table table-hover align-middle mb-0">
|
||
|
|
<thead class="bg-light">
|
||
|
|
<tr>
|
||
|
|
<th>Abonnement</th>
|
||
|
|
<th>Kunde</th>
|
||
|
|
<th>Sag</th>
|
||
|
|
<th>Produkt</th>
|
||
|
|
<th>Interval</th>
|
||
|
|
<th>Pris</th>
|
||
|
|
<th>Start</th>
|
||
|
|
<th>Status</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody id="subscriptionsBody">
|
||
|
|
<tr>
|
||
|
|
<td colspan="8" class="text-center text-muted py-5">
|
||
|
|
<span class="spinner-border spinner-border-sm me-2"></span>Indlaeser...
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
async function loadSubscriptions() {
|
||
|
|
try {
|
||
|
|
const stats = await fetch('/api/v1/subscriptions/stats/summary').then(r => r.json());
|
||
|
|
document.getElementById('activeCount').textContent = stats.subscription_count || 0;
|
||
|
|
document.getElementById('totalAmount').textContent = formatCurrency(stats.total_amount || 0);
|
||
|
|
document.getElementById('avgAmount').textContent = formatCurrency(stats.avg_amount || 0);
|
||
|
|
|
||
|
|
const subscriptions = await fetch('/api/v1/subscriptions').then(r => r.json());
|
||
|
|
renderSubscriptions(subscriptions);
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Error loading subscriptions:', e);
|
||
|
|
document.getElementById('subscriptionsBody').innerHTML = `
|
||
|
|
<tr><td colspan="8" class="text-center text-danger py-5">
|
||
|
|
<i class="bi bi-exclamation-triangle fs-1 mb-3"></i>
|
||
|
|
<p>Fejl ved indlaesning</p>
|
||
|
|
</td></tr>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderSubscriptions(subscriptions) {
|
||
|
|
const tbody = document.getElementById('subscriptionsBody');
|
||
|
|
|
||
|
|
if (!subscriptions || subscriptions.length === 0) {
|
||
|
|
tbody.innerHTML = `
|
||
|
|
<tr><td colspan="8" class="text-center text-muted py-5">
|
||
|
|
<i class="bi bi-inbox fs-1 mb-3"></i>
|
||
|
|
<p>Ingen aktive abonnementer</p>
|
||
|
|
</td></tr>
|
||
|
|
`;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
tbody.innerHTML = subscriptions.map(sub => {
|
||
|
|
const intervalLabel = formatInterval(sub.billing_interval);
|
||
|
|
const statusBadge = getStatusBadge(sub.status);
|
||
|
|
const sagLink = sub.sag_id ? `<a href="/sag/${sub.sag_id}">${sub.sag_title || 'Sag #' + sub.sag_id}</a>` : '-';
|
||
|
|
const subNumber = sub.subscription_number || `#${sub.id}`;
|
||
|
|
|
||
|
|
return `
|
||
|
|
<tr>
|
||
|
|
<td><strong>${subNumber}</strong></td>
|
||
|
|
<td>${sub.customer_name || '-'}</td>
|
||
|
|
<td>${sagLink}</td>
|
||
|
|
<td>${sub.product_name || '-'}</td>
|
||
|
|
<td>${intervalLabel}${sub.billing_day ? ' (dag ' + sub.billing_day + ')' : ''}</td>
|
||
|
|
<td>${formatCurrency(sub.price || 0)}</td>
|
||
|
|
<td>${formatDate(sub.start_date)}</td>
|
||
|
|
<td>${statusBadge}</td>
|
||
|
|
</tr>
|
||
|
|
`;
|
||
|
|
}).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatInterval(interval) {
|
||
|
|
const map = {
|
||
|
|
'monthly': 'Maaned',
|
||
|
|
'quarterly': 'Kvartal',
|
||
|
|
'yearly': 'Aar'
|
||
|
|
};
|
||
|
|
return map[interval] || interval || '-';
|
||
|
|
}
|
||
|
|
|
||
|
|
function getStatusBadge(status) {
|
||
|
|
const badges = {
|
||
|
|
'active': '<span class="badge bg-success">Aktiv</span>',
|
||
|
|
'paused': '<span class="badge bg-warning">Pauset</span>',
|
||
|
|
'cancelled': '<span class="badge bg-secondary">Opsagt</span>',
|
||
|
|
'draft': '<span class="badge bg-light text-dark">Kladde</span>'
|
||
|
|
};
|
||
|
|
return badges[status] || status || '-';
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatCurrency(amount) {
|
||
|
|
return new Intl.NumberFormat('da-DK', {
|
||
|
|
style: 'currency',
|
||
|
|
currency: 'DKK',
|
||
|
|
minimumFractionDigits: 0,
|
||
|
|
maximumFractionDigits: 0
|
||
|
|
}).format(amount);
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatDate(dateStr) {
|
||
|
|
if (!dateStr) return '-';
|
||
|
|
const date = new Date(dateStr);
|
||
|
|
return date.toLocaleDateString('da-DK');
|
||
|
|
}
|
||
|
|
|
||
|
|
document.addEventListener('DOMContentLoaded', loadSubscriptions);
|
||
|
|
</script>
|
||
|
|
{% endblock %}
|