bmc_hub/app/modules/sag/templates/varekob_salg.html
Christian 56d6d45aa2 feat(sag): Add Varekøb & Salg module with database migration and frontend template
- Created a new SQL migration for the sag_salgsvarer table to manage sales and purchase items.
- Implemented a new HTML template for the Varekøb & Salg module, including summary cards and tables for sales and purchases.
- Added JavaScript functions for loading and rendering order data dynamically.
- Introduced a new backend search module for customers, contacts, hardware, and locations with autocomplete functionality.
- Developed an email templates API for managing system and customer-specific email templates.
- Created multiple migrations for Nextcloud instances, cache, audit logs, email templates, sag comments, hardware locations, and billing methods.
- Enhanced the sag module with solutions, order lines, work types, and 2FA support for user authentication.
2026-02-02 20:23:56 +01:00

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 %}