bmc_hub/app/subscriptions/frontend/list_backup.html
Christian 3cddb71cec feat: Add Technician Dashboard V1, V2, and V3 with enhanced UI and functionality
- Introduced Technician Dashboard V1 (tech_v1_overview.html) with KPI cards and new cases overview.
- Implemented Technician Dashboard V2 (tech_v2_workboard.html) featuring a workboard layout for daily tasks and opportunities.
- Developed Technician Dashboard V3 (tech_v3_table_focus.html) with a power table for detailed case management.
- Created a dashboard selector page (technician_dashboard_selector.html) for easy navigation between dashboard versions.
- Added user dashboard preferences migration (130_user_dashboard_preferences.sql) to store default dashboard paths.
- Enhanced sag_sager table with assigned group ID (131_sag_assignment_group.sql) for better case management.
- Updated sag_subscriptions table to include cancellation rules and billing dates (132_subscription_cancellation.sql, 134_subscription_billing_dates.sql).
- Implemented subscription staging for CRM integration (136_simply_subscription_staging.sql).
- Added a script to move time tracking section in detail view (move_time_section.py).
- Created a test script for subscription processing (test_subscription_processing.py).
2026-02-17 08:29:05 +01:00

193 lines
7.0 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 class="col-auto">
<select class="form-select" id="subscriptionStatusFilter" style="min-width: 180px;">
<option value="all" selected>Alle statuser</option>
<option value="active">Aktiv</option>
<option value="paused">Pauset</option>
<option value="cancelled">Opsagt</option>
<option value="draft">Kladde</option>
</select>
</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" id="subscriptionsTitle">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 status = document.getElementById('subscriptionStatusFilter')?.value || 'all';
const stats = await fetch(`/api/v1/subscriptions/stats/summary?status=${encodeURIComponent(status)}`).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?status=${encodeURIComponent(status)}`).then(r => r.json());
renderSubscriptions(subscriptions);
const title = document.getElementById('subscriptionsTitle');
if (title) {
const labelMap = {
all: 'Alle abonnementer',
active: 'Aktive abonnementer',
paused: 'Pausede abonnementer',
cancelled: 'Opsagte abonnementer',
draft: 'Kladder'
};
title.textContent = labelMap[status] || 'Abonnementer';
}
} 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', () => {
const filter = document.getElementById('subscriptionStatusFilter');
if (filter) {
filter.addEventListener('change', loadSubscriptions);
}
loadSubscriptions();
});
</script>
{% endblock %}