bmc_hub/app/products/frontend/detail.html

367 lines
15 KiB
HTML
Raw Normal View History

{% extends "shared/frontend/base.html" %}
{% block title %}Produkt - BMC Hub{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Serif:wght@400;500;600&display=swap">
<style>
:root {
--product-bg-1: #f5f7fb;
--product-bg-2: #e9f1f7;
--product-ink: #102a43;
--product-ink-muted: #486581;
--product-accent: #0f4c75;
--product-card: #ffffff;
--product-border: rgba(15, 76, 117, 0.12);
}
[data-bs-theme="dark"] {
--product-bg-1: #1b1f24;
--product-bg-2: #20252b;
--product-ink: #f1f5f9;
--product-ink-muted: #b6c2d1;
--product-card: #252b33;
--product-border: rgba(61, 139, 253, 0.18);
}
.product-page {
font-family: "Space Grotesk", sans-serif;
color: var(--product-ink);
background: radial-gradient(circle at 10% 10%, var(--product-bg-2), transparent 45%),
radial-gradient(circle at 90% 20%, rgba(15, 76, 117, 0.15), transparent 50%),
linear-gradient(160deg, var(--product-bg-1), var(--product-bg-2));
border-radius: 18px;
padding: 28px;
box-shadow: 0 20px 40px rgba(15, 76, 117, 0.08);
position: relative;
overflow: hidden;
}
.product-page h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 6px;
}
.product-muted {
color: var(--product-ink-muted);
}
.product-card {
background: var(--product-card);
border-radius: 16px;
border: 1px solid var(--product-border);
padding: 18px;
box-shadow: 0 16px 30px rgba(15, 76, 117, 0.08);
}
.product-table th {
background: rgba(15, 76, 117, 0.06);
}
.badge-soft {
background: rgba(15, 76, 117, 0.12);
color: var(--product-ink);
border-radius: 999px;
padding: 4px 10px;
font-size: 0.75rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="product-page">
<div class="d-flex flex-wrap align-items-start justify-content-between gap-3 mb-4">
<div>
<a href="/products" class="text-decoration-none product-muted">&#8592; Tilbage til produkter</a>
<h1 id="productName">Produkt</h1>
<div class="product-muted" id="productMeta"></div>
</div>
<div class="text-end">
<div class="badge-soft" id="productStatus">-</div>
<div class="fs-3 fw-semibold mt-2" id="productPrice">-</div>
<div class="product-muted" id="productSku">-</div>
<div class="product-muted" id="productSupplierPrice">-</div>
</div>
</div>
<div class="row g-3">
<div class="col-lg-4">
<div class="product-card h-100">
<h5 class="mb-3">Opdater pris</h5>
<div class="mb-3">
<label class="form-label">Ny salgspris</label>
<input type="number" class="form-control" id="priceNewValue" step="0.01" min="0">
</div>
<div class="mb-3">
<label class="form-label">Note</label>
<input type="text" class="form-control" id="priceNote" placeholder="Aarsag til prisændring">
</div>
<div class="mb-3">
<label class="form-label">Opdateret af</label>
<input type="text" class="form-control" id="priceChangedBy" placeholder="Navn">
</div>
<button class="btn btn-primary w-100" onclick="submitPriceUpdate()">Gem pris</button>
<div class="small mt-3" id="priceUpdateMessage"></div>
</div>
<div class="product-card mt-3">
<h5 class="mb-3">Opdater leverandoer</h5>
<div class="mb-3">
<label class="form-label">Leverandoer</label>
<input type="text" class="form-control" id="supplierName" placeholder="Leverandoer navn">
</div>
<div class="mb-3">
<label class="form-label">Leverandoer pris</label>
<input type="number" class="form-control" id="supplierPrice" step="0.01" min="0">
</div>
<div class="mb-3">
<label class="form-label">Note</label>
<input type="text" class="form-control" id="supplierNote" placeholder="Aarsag til ændring">
</div>
<div class="mb-3">
<label class="form-label">Opdateret af</label>
<input type="text" class="form-control" id="supplierChangedBy" placeholder="Navn">
</div>
<button class="btn btn-outline-primary w-100" onclick="submitSupplierUpdate()">Gem leverandoer</button>
<div class="small mt-3" id="supplierUpdateMessage"></div>
</div>
</div>
<div class="col-lg-8">
<div class="product-card mb-3">
<h5 class="mb-3">Pris historik</h5>
<div class="table-responsive">
<table class="table table-sm product-table mb-0">
<thead>
<tr>
<th>Dato</th>
<th>Type</th>
<th>Gammel</th>
<th>Ny</th>
<th>Note</th>
<th>Af</th>
</tr>
</thead>
<tbody id="priceHistoryBody">
<tr><td colspan="6" class="text-center product-muted py-3">Indlaeser...</td></tr>
</tbody>
</table>
</div>
</div>
<div class="product-card">
<h5 class="mb-3">Tidligere salg</h5>
<div class="table-responsive">
<table class="table table-sm product-table mb-0">
<thead>
<tr>
<th>Dato</th>
<th>Kilde</th>
<th>Beskrivelse</th>
<th>Antal</th>
<th>Pris</th>
<th>Total</th>
<th>Status</th>
</tr>
</thead>
<tbody id="salesHistoryBody">
<tr><td colspan="7" class="text-center product-muted py-3">Indlaeser...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const productId = {{ product_id }};
function escapeHtml(value) {
return String(value || '').replace(/[&<>"']/g, (ch) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[ch]));
}
function formatCurrency(amount) {
return new Intl.NumberFormat('da-DK', {
style: 'currency',
currency: 'DKK',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(amount || 0);
}
function formatDate(value) {
if (!value) return '-';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleDateString('da-DK');
}
function setMessage(message, tone = 'text-muted') {
const el = document.getElementById('priceUpdateMessage');
if (!el) return;
el.className = `small ${tone}`;
el.textContent = message;
}
function setSupplierMessage(message, tone = 'text-muted') {
const el = document.getElementById('supplierUpdateMessage');
if (!el) return;
el.className = `small ${tone}`;
el.textContent = message;
}
async function loadProductDetail() {
try {
const res = await fetch(`/api/v1/products/${productId}`);
if (!res.ok) throw new Error('Kunne ikke hente produkt');
const product = await res.json();
document.getElementById('productName').textContent = product.name || 'Produkt';
document.getElementById('productMeta').textContent = [
product.manufacturer,
product.type,
product.supplier_name
].filter(Boolean).join(' • ');
document.getElementById('productStatus').textContent = product.status || '-';
document.getElementById('productPrice').textContent = product.sales_price != null ? formatCurrency(product.sales_price) : '-';
document.getElementById('productSku').textContent = product.sku_internal || '-';
document.getElementById('productSupplierPrice').textContent = product.supplier_price != null
? `Leverandoer: ${formatCurrency(product.supplier_price)}`
: 'Leverandoer pris: -';
document.getElementById('priceNewValue').value = product.sales_price != null ? product.sales_price : '';
document.getElementById('supplierName').value = product.supplier_name || '';
document.getElementById('supplierPrice').value = product.supplier_price != null ? product.supplier_price : '';
} catch (e) {
setMessage(e.message || 'Fejl ved indlaesning', 'text-danger');
}
}
async function loadPriceHistory() {
const tbody = document.getElementById('priceHistoryBody');
try {
const res = await fetch(`/api/v1/products/${productId}/price-history`);
if (!res.ok) throw new Error('Kunne ikke hente pris historik');
const history = await res.json();
if (!history.length) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center product-muted py-3">Ingen historik</td></tr>';
return;
}
tbody.innerHTML = history.map(entry => `
<tr>
<td>${formatDate(entry.changed_at)}</td>
<td>${entry.price_type === 'supplier_price' ? 'Leverandoer' : 'Salgspris'}</td>
<td>${entry.old_price != null ? formatCurrency(entry.old_price) : '-'}</td>
<td>${entry.new_price != null ? formatCurrency(entry.new_price) : '-'}</td>
<td>${escapeHtml(entry.note || '-')}</td>
<td>${escapeHtml(entry.changed_by || '-')}</td>
</tr>
`).join('');
} catch (e) {
tbody.innerHTML = `<tr><td colspan="6" class="text-center text-danger py-3">${escapeHtml(e.message || 'Fejl')}</td></tr>`;
}
}
async function loadSalesHistory() {
const tbody = document.getElementById('salesHistoryBody');
try {
const res = await fetch(`/api/v1/products/${productId}/sales-history`);
if (!res.ok) throw new Error('Kunne ikke hente salgs historik');
const history = await res.json();
if (!history.length) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center product-muted py-3">Ingen salg fundet</td></tr>';
return;
}
tbody.innerHTML = history.map(entry => `
<tr>
<td>${formatDate(entry.line_date)}</td>
<td>${entry.source === 'subscription' ? 'Abonnement' : 'Sag salg'}</td>
<td>${escapeHtml(entry.description || '-')}</td>
<td>${entry.quantity != null ? entry.quantity : '-'}</td>
<td>${entry.unit_price != null ? formatCurrency(entry.unit_price) : '-'}</td>
<td>${entry.total_amount != null ? formatCurrency(entry.total_amount) : '-'}</td>
<td>${escapeHtml(entry.status || '-')}</td>
</tr>
`).join('');
} catch (e) {
tbody.innerHTML = `<tr><td colspan="7" class="text-center text-danger py-3">${escapeHtml(e.message || 'Fejl')}</td></tr>`;
}
}
async function submitPriceUpdate() {
const newPrice = document.getElementById('priceNewValue').value;
const note = document.getElementById('priceNote').value.trim();
const changedBy = document.getElementById('priceChangedBy').value.trim();
if (!newPrice) {
setMessage('Angiv ny pris', 'text-danger');
return;
}
try {
const res = await fetch(`/api/v1/products/${productId}/price`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
new_price: Number(newPrice),
note: note || null,
changed_by: changedBy || null
})
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.detail || 'Prisopdatering fejlede');
}
await res.json();
setMessage('Pris opdateret', 'text-success');
await loadProductDetail();
await loadPriceHistory();
} catch (e) {
setMessage(e.message || 'Fejl ved opdatering', 'text-danger');
}
}
async function submitSupplierUpdate() {
const supplierName = document.getElementById('supplierName').value.trim();
const supplierPriceValue = document.getElementById('supplierPrice').value;
const note = document.getElementById('supplierNote').value.trim();
const changedBy = document.getElementById('supplierChangedBy').value.trim();
try {
const res = await fetch(`/api/v1/products/${productId}/supplier`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
supplier_name: supplierName || null,
supplier_price: supplierPriceValue ? Number(supplierPriceValue) : null,
note: note || null,
changed_by: changedBy || null
})
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.detail || 'Leverandoer opdatering fejlede');
}
await res.json();
setSupplierMessage('Leverandoer opdateret', 'text-success');
await loadProductDetail();
await loadPriceHistory();
} catch (e) {
setSupplierMessage(e.message || 'Fejl ved opdatering', 'text-danger');
}
}
document.addEventListener('DOMContentLoaded', () => {
loadProductDetail();
loadPriceHistory();
loadSalesHistory();
});
</script>
{% endblock %}