bmc_hub/app/modules/sag/templates/varekob_salg.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

366 lines
15 KiB
HTML

{% extends "shared/frontend/base.html" %}
{% block title %}Varekøb & Salg - BMC Hub{% endblock %}
{% block extra_css %}
<style>
.summary-card {
background: var(--bg-card);
border-radius: 12px;
padding: 1rem 1.25rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
border: 1px solid rgba(0,0,0,0.05);
}
.summary-title {
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.6px;
color: var(--text-secondary);
margin-bottom: 0.35rem;
}
.summary-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-primary);
}
.table-wrapper {
background: var(--bg-card);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.table thead th {
background: var(--accent);
color: white;
border: none;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.chip {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.25rem 0.6rem;
border-radius: 999px;
font-size: 0.75rem;
border: 1px solid rgba(0,0,0,0.1);
background: var(--bg-light);
}
.table-secondary {
background-color: rgba(var(--accent-rgb, 15, 76, 117), 0.08) !important;
cursor: pointer;
transition: background-color 0.2s;
}
.table-secondary:hover {
background-color: rgba(var(--accent-rgb, 15, 76, 117), 0.15) !important;
}
.table-secondary td {
padding: 0.75rem !important;
}
.case-lines-container {
display: none;
}
.case-lines-container.show {
display: table-row;
}
.expand-icon {
transition: transform 0.3s;
}
.expand-icon.expanded {
transform: rotate(90deg);
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="d-flex flex-wrap justify-content-between align-items-center mb-3">
<div>
<h2 class="mb-1"><i class="bi bi-basket3 me-2"></i>Varekøb & Salg</h2>
<div class="text-muted">Samlet oversigt over alle varelinjer på tværs af sager</div>
</div>
<div class="d-flex gap-2">
<a class="btn btn-outline-primary" href="/sag"><i class="bi bi-arrow-left me-1"></i>Tilbage til sager</a>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="summary-card">
<div class="summary-title">Total salg</div>
<div class="summary-value" id="summarySalesTotal">-</div>
</div>
</div>
<div class="col-md-3">
<div class="summary-card">
<div class="summary-title">Total køb</div>
<div class="summary-value" id="summaryPurchaseTotal">-</div>
</div>
</div>
<div class="col-md-3">
<div class="summary-card">
<div class="summary-title">Netto</div>
<div class="summary-value" id="summaryNetTotal">-</div>
</div>
</div>
<div class="col-md-3">
<div class="summary-card">
<div class="summary-title">Linjer (total)</div>
<div class="summary-value" id="summaryLinesTotal">-</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-4">
<label class="form-label">Søg</label>
<input type="text" class="form-control" id="ordersSearch" placeholder="Søg i beskrivelse, sag eller kunde">
</div>
<div class="col-md-3">
<label class="form-label">Status</label>
<select class="form-select" id="ordersStatus">
<option value="">Alle</option>
<option value="draft">Kladde</option>
<option value="confirmed">Bekræftet</option>
<option value="cancelled">Annulleret</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Sag ID</label>
<input type="number" class="form-control" id="ordersCaseId" placeholder="F.eks. 12">
</div>
<div class="col-md-3">
<label class="form-label">Kunde ID</label>
<input type="number" class="form-control" id="ordersCustomerId" placeholder="F.eks. 45">
</div>
<div class="col-md-2">
<label class="form-label">Fra dato</label>
<input type="date" class="form-control" id="ordersDateFrom">
</div>
<div class="col-md-2">
<label class="form-label">Til dato</label>
<input type="date" class="form-control" id="ordersDateTo">
</div>
<div class="col-md-2">
<button class="btn btn-primary w-100" onclick="loadOrders()"><i class="bi bi-search me-1"></i>Filtrér</button>
</div>
<div class="col-md-3 text-end">
<span class="chip"><i class="bi bi-info-circle"></i>Alle sager</span>
</div>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-lg-6">
<div class="table-wrapper">
<div class="d-flex justify-content-between align-items-center px-3 py-2 border-bottom">
<h6 class="mb-0 text-primary"><i class="bi bi-bag-check me-2"></i>Salgslinjer</h6>
<span class="badge bg-light text-dark border" id="salesSubtotal">-</span>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0" style="vertical-align: middle;">
<thead>
<tr>
<th style="width: 30px;"></th>
<th>Dato</th>
<th>Beskrivelse</th>
<th>Kunde</th>
<th>Antal</th>
<th>Enhed</th>
<th>Enhedspris</th>
<th>Linjesum</th>
<th>Status</th>
</tr>
</thead>
<tbody id="ordersSalesBody">
<tr>
<td colspan="9" class="text-center py-4 text-muted">Indlæser...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="table-wrapper">
<div class="d-flex justify-content-between align-items-center px-3 py-2 border-bottom">
<h6 class="mb-0 text-primary"><i class="bi bi-cart-x me-2"></i>Indkøbslinjer</h6>
<span class="badge bg-light text-dark border" id="purchaseSubtotal">-</span>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0" style="vertical-align: middle;">
<thead>
<tr>
<th style="width: 30px;"></th>
<th>Dato</th>
<th>Beskrivelse</th>
<th>Kunde</th>
<th>Antal</th>
<th>Enhed</th>
<th>Enhedspris</th>
<th>Linjesum</th>
<th>Status</th>
</tr>
</thead>
<tbody id="ordersPurchaseBody">
<tr>
<td colspan="9" class="text-center py-4 text-muted">Indlæser...</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function formatCurrency(value) {
const num = Number(value || 0);
return new Intl.NumberFormat('da-DK', { style: 'currency', currency: 'DKK' }).format(num);
}
function formatNumber(value) {
const num = Number(value || 0);
return new Intl.NumberFormat('da-DK', { minimumFractionDigits: 0, maximumFractionDigits: 2 }).format(num);
}
function renderOrderRows(items, tbodyId) {
const tbody = document.getElementById(tbodyId);
if (!tbody) return;
if (!items.length) {
tbody.innerHTML = '<tr><td colspan="9" class="text-center py-4 text-muted">Ingen linjer</td></tr>';
return;
}
// Group items by case (sag_id)
const grouped = {};
items.forEach((item, originalIndex) => {
const caseKey = item.sag_id || 'ingen-sag';
if (!grouped[caseKey]) {
grouped[caseKey] = {
sag_id: item.sag_id || null,
sag_titel: item.sag_titel || 'Ingen sag',
items: []
};
}
grouped[caseKey].items.push({ ...item, originalIndex });
});
// Render grouped rows with collapsible structure
let html = '';
Object.keys(grouped).forEach((caseKey, groupIndex) => {
const group = grouped[caseKey];
const groupTotal = group.items.reduce((sum, item) => sum + Number(item.amount || 0), 0);
const tableId = tbodyId.replace('Body', ''); // Extract table identifier
const groupId = `${tableId}-case-${groupIndex}`;
// Case header row (clickable to expand/collapse)
const caseLink = group.sag_id
? `<a href="/sag/${group.sag_id}" class="text-decoration-none fw-bold" onclick="event.stopPropagation();">${group.sag_titel} <span class="badge bg-light text-dark border">Sag ${group.sag_id}</span></a>`
: `<span class="fw-bold">${group.sag_titel}</span>`;
html += `
<tr class="table-secondary" onclick="toggleCaseLines('${groupId}')">
<td>
<i class="bi bi-chevron-right expand-icon" id="icon-${groupId}"></i>
</td>
<td colspan="8">
<div class="d-flex justify-content-between align-items-center">
<div>${caseLink}</div>
<div class="text-end">
<span class="badge bg-primary me-2">${group.items.length} ${group.items.length === 1 ? 'linje' : 'linjer'}</span>
<span class="fw-bold">${formatCurrency(groupTotal)}</span>
</div>
</div>
</td>
</tr>
`;
// Render lines in this case (hidden by default)
group.items.forEach(item => {
const statusLabel = item.status || 'draft';
html += `
<tr class="case-lines-container" data-case="${groupId}">
<td></td>
<td>${item.line_date || '-'}</td>
<td>${item.description || '-'}</td>
<td>${item.customer_name || '-'}</td>
<td>${item.quantity ?? '-'}</td>
<td>${item.unit || '-'}</td>
<td>${item.unit_price != null ? formatCurrency(item.unit_price) : '-'}</td>
<td class="fw-bold">${formatCurrency(item.amount)}</td>
<td><span class="badge bg-light text-dark border">${statusLabel}</span></td>
</tr>
`;
});
});
tbody.innerHTML = html;
}
function toggleCaseLines(caseId) {
const lines = document.querySelectorAll(`tr[data-case="${caseId}"]`);
const icon = document.getElementById(`icon-${caseId}`);
lines.forEach(line => {
line.classList.toggle('show');
});
if (icon) {
icon.classList.toggle('expanded');
}
}
async function loadOrders() {
const search = document.getElementById('ordersSearch').value.trim();
const status = document.getElementById('ordersStatus').value;
const caseId = document.getElementById('ordersCaseId').value;
const customerId = document.getElementById('ordersCustomerId').value;
const dateFrom = document.getElementById('ordersDateFrom').value;
const dateTo = document.getElementById('ordersDateTo').value;
const params = new URLSearchParams();
if (search) params.append('q', search);
if (status) params.append('status', status);
if (caseId) params.append('sag_id', caseId);
if (customerId) params.append('customer_id', customerId);
if (dateFrom) params.append('date_from', dateFrom);
if (dateTo) params.append('date_to', dateTo);
const res = await fetch(`/api/v1/sag/sale-items/all?${params.toString()}`);
if (!res.ok) {
document.getElementById('ordersSalesBody').innerHTML = '<tr><td colspan="9" class="text-center py-4 text-muted">Kunne ikke hente data</td></tr>';
document.getElementById('ordersPurchaseBody').innerHTML = '<tr><td colspan="9" class="text-center py-4 text-muted">Kunne ikke hente data</td></tr>';
return;
}
const data = await res.json();
const sales = data.filter(item => (item.type || '').toLowerCase() !== 'purchase');
const purchases = data.filter(item => (item.type || '').toLowerCase() === 'purchase');
renderOrderRows(sales, 'ordersSalesBody');
renderOrderRows(purchases, 'ordersPurchaseBody');
const salesSum = sales.reduce((sum, item) => sum + Number(item.amount || 0), 0);
const purchaseSum = purchases.reduce((sum, item) => sum + Number(item.amount || 0), 0);
document.getElementById('summarySalesTotal').textContent = formatCurrency(salesSum);
document.getElementById('summaryPurchaseTotal').textContent = formatCurrency(purchaseSum);
document.getElementById('summaryNetTotal').textContent = formatCurrency(salesSum - purchaseSum);
document.getElementById('summaryLinesTotal').textContent = formatNumber(data.length);
document.getElementById('salesSubtotal').textContent = formatCurrency(salesSum);
document.getElementById('purchaseSubtotal').textContent = formatCurrency(purchaseSum);
}
document.addEventListener('DOMContentLoaded', () => {
loadOrders();
});
</script>
{% endblock %}