283 lines
12 KiB
HTML
283 lines
12 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);
|
||
|
|
}
|
||
|
|
</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>Dato</th>
|
||
|
|
<th>Beskrivelse</th>
|
||
|
|
<th>Sag</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>Dato</th>
|
||
|
|
<th>Beskrivelse</th>
|
||
|
|
<th>Sag</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;
|
||
|
|
}
|
||
|
|
|
||
|
|
tbody.innerHTML = items.map(item => {
|
||
|
|
const statusLabel = item.status || 'draft';
|
||
|
|
const caseLink = item.sag_id ? `<a href="/sag/${item.sag_id}" class="text-decoration-none">${item.sag_titel || 'Sag ' + item.sag_id}</a>` : '-';
|
||
|
|
return `
|
||
|
|
<tr>
|
||
|
|
<td>${item.line_date || '-'}</td>
|
||
|
|
<td>${item.description || '-'}</td>
|
||
|
|
<td>${caseLink}</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>
|
||
|
|
`;
|
||
|
|
}).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
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 %}
|