@@ -0,0 +1,112 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script src="/auth-guard.js?v=4"></script>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
|
||||
<title>Sales Reports — SAP B1</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
:root{--ink:#080e18;--blue:#1a6fff;--amber:#f5a623;--green:#0dbb8a;--red:#e53e3e;--border:#1c2e48;--border2:#243650;--text:#c8d6e8;--text2:#7a9ab8;--text3:#3d5470;--surface:#0f1d30;--surface2:#142338;--surface3:#0b1520;--radius:10px;--font:'Space Grotesk',sans-serif;--mono:'JetBrains Mono',monospace}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:var(--font);background:var(--ink);color:var(--text);min-height:100vh}
|
||||
main{padding:12px;max-width:1500px;margin:0 auto}
|
||||
.page-title{font-size:16px;font-weight:700;margin-bottom:10px}
|
||||
.tabs{display:flex;gap:4px;margin-bottom:10px;border-bottom:1px solid var(--border);overflow-x:auto}
|
||||
.tab{padding:8px 14px;font-size:12.5px;font-weight:700;color:var(--text2);cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap}.tab.on{color:var(--blue);border-bottom-color:var(--blue)}
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:12px;overflow:hidden}
|
||||
.card-hd{background:var(--surface3);padding:10px 14px;font-size:12px;font-weight:700;display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--border)}
|
||||
.card-bd{padding:12px 14px}
|
||||
.filters{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end}.filters>div{min-width:140px}
|
||||
.kpis{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px;margin-bottom:12px}
|
||||
.kpi{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:12px 14px}
|
||||
.kpi b{display:block;font-size:10px;text-transform:uppercase;letter-spacing:.4px;color:var(--text3)}.kpi span{font-size:20px;font-weight:700}
|
||||
.lst{width:100%;border-collapse:separate;border-spacing:0;font-size:12px}
|
||||
.lst th{text-align:left;padding:8px 10px;font-size:9px;font-weight:700;text-transform:uppercase;color:var(--text3);border-bottom:2px solid var(--border2);white-space:nowrap;background:var(--surface3);position:sticky;top:0}
|
||||
.lst td{padding:7px 10px;border-bottom:1px solid var(--border);white-space:nowrap}
|
||||
.lst td.n,.lst th.n{text-align:right;font-family:var(--mono)}
|
||||
.tbl-wrap{overflow:auto;max-height:68vh}
|
||||
.two{display:grid;grid-template-columns:1fr 1fr;gap:12px}@media(max-width:900px){.two{grid-template-columns:1fr}}
|
||||
.mono{font-family:var(--mono)}.empty{text-align:center;padding:26px;color:var(--text3);font-size:12px}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/theme.css?v=3"/>
|
||||
<script src="/sales-common.js?v=4"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main id="app"><div class="empty"><span class="sc-spin"></span></div></main>
|
||||
<script>
|
||||
'use strict';
|
||||
const { esc, money, num, date, q, qa, toast, api } = SC;
|
||||
let ME = null, TAB = 'dashboard', DATA = null;
|
||||
const divName = id => ((ME.allDivisions || ME.divisions).find(d => d.id === id) || {}).name || id; // incl. disabled categories (existing orders)
|
||||
const TABS = [['dashboard', 'Dashboard'], ['order-wise', 'Order-wise'], ['item-wise', 'Item-wise'], ['pending', 'Pending (open in SAP)'], ['customer-pending', 'Customer-wise pending']];
|
||||
|
||||
async function init() { ME = await api('/me'); render(); }
|
||||
function render() {
|
||||
const firstOfYear = `${new Date().getMonth() >= 3 ? new Date().getFullYear() : new Date().getFullYear() - 1}-04-01`;
|
||||
q('#app').innerHTML = `<div class="page-title">Sales Reports</div>
|
||||
<div class="tabs">${TABS.map(([k, l]) => `<div class="tab ${TAB === k ? 'on' : ''}" data-t="${k}">${l}</div>`).join('')}</div>
|
||||
<div class="card"><div class="card-bd"><div class="filters">
|
||||
<div><label class="sc-lbl">Product category</label><select class="sc-in" id="f-div"><option value="">All mine</option>${ME.divisions.filter(d => ME.profile.divisions.includes(d.id)).map(d => `<option value="${d.id}">${esc(d.name)}</option>`).join('')}</select></div>
|
||||
<div><label class="sc-lbl">Type</label><select class="sc-in" id="f-type"><option value="">All</option><option value="1">Trade</option><option value="2">Institution</option></select></div>
|
||||
${['pending', 'customer-pending'].includes(TAB) ? '' : `<div><label class="sc-lbl">From</label><input class="sc-in" type="date" id="f-from" value="${firstOfYear}"></div><div><label class="sc-lbl">To</label><input class="sc-in" type="date" id="f-to"></div>`}
|
||||
<div style="min-width:auto"><button class="sc-btn sc-b-primary" id="f-go">Run</button></div>
|
||||
${TAB === 'dashboard' ? '' : '<div style="min-width:auto"><button class="sc-btn sc-b-ghost" id="f-csv">⬇ CSV</button></div>'}</div></div></div>
|
||||
<div id="out"></div>`;
|
||||
qa('.tab').forEach(t => { t.onclick = () => { TAB = t.dataset.t; render(); }; });
|
||||
q('#f-go').onclick = run; if (q('#f-csv')) q('#f-csv').onclick = csv;
|
||||
run();
|
||||
}
|
||||
async function run() {
|
||||
const p = new URLSearchParams({ divisionId: q('#f-div').value, soType: q('#f-type').value });
|
||||
if (q('#f-from')) { p.set('from', q('#f-from').value); p.set('to', q('#f-to').value); }
|
||||
q('#out').innerHTML = '<div class="empty"><span class="sc-spin"></span> Running…</div>';
|
||||
try { DATA = await api(`/reports/${TAB}?` + p); show(); } catch (e) { q('#out').innerHTML = `<div class="sc-err">${esc(e.message)}</div>`; }
|
||||
}
|
||||
function show() {
|
||||
const d = DATA, o = q('#out');
|
||||
if (TAB === 'dashboard') {
|
||||
const k = d.kpis; const t = (l, x, extra) => `<div class="kpi"><b>${l}</b><span>${x ? x.n : 0}</span><div class="sc-muted">${x ? money(x.v) : ''}${extra || ''}</div></div>`;
|
||||
o.innerHTML = `<div class="kpis">${t('Orders (excl. cancelled)', k.total)}${t('Awaiting MIPL action', k.awaitingAction)}${t('Awaiting payment / verification', k.awaitingPayment)}
|
||||
${t('Ready to post in SAP', k.readyForSap)}${t('In SAP (invoicing)', k.inSap)}${t('Closed', k.closed)}${t('Cancelled', k.cancelled)}
|
||||
<div class="kpi"><b>Avg. days order → SAP</b><span>${k.avgDaysToSap == null ? '—' : k.avgDaysToSap}</span></div></div>
|
||||
<div class="two"><div class="card"><div class="card-hd">Monthly (last 12 months)</div><div class="tbl-wrap"><table class="lst"><thead><tr><th>Month</th><th class="n">Orders</th><th class="n">Value</th></tr></thead><tbody>
|
||||
${d.monthly.map(m => `<tr><td>${esc(m.month)}</td><td class="n">${m.count}</td><td class="n">${money(m.value)}</td></tr>`).join('') || '<tr><td colspan="3" class="empty">No data</td></tr>'}</tbody></table></div></div>
|
||||
<div class="card"><div class="card-hd">Top customers</div><div class="tbl-wrap"><table class="lst"><thead><tr><th>Customer</th><th class="n">Orders</th><th class="n">Value</th></tr></thead><tbody>
|
||||
${d.topCustomers.map(c => `<tr><td>${esc(c.cardName)} <span class="sc-muted">${esc(c.cardCode)}</span></td><td class="n">${c.count}</td><td class="n">${money(c.value)}</td></tr>`).join('') || '<tr><td colspan="3" class="empty">No data</td></tr>'}</tbody></table></div></div>
|
||||
<div class="card"><div class="card-hd">By product category</div><div class="tbl-wrap"><table class="lst"><thead><tr><th>Category</th><th class="n">Orders</th><th class="n">Value</th></tr></thead><tbody>
|
||||
${d.byDivision.map(c => `<tr><td>${esc(c.division)}</td><td class="n">${c.count}</td><td class="n">${money(c.value)}</td></tr>`).join('') || '<tr><td colspan="3" class="empty">No data</td></tr>'}</tbody></table></div></div>
|
||||
<div class="card"><div class="card-hd">By status</div><div class="tbl-wrap"><table class="lst"><thead><tr><th>Status</th><th class="n">Orders</th><th class="n">Value</th></tr></thead><tbody>
|
||||
${d.byStatus.sort((a, b) => a.status - b.status).map(c => `<tr><td>${SC.pill(c.status, c.label)}${c.orderType === 'DIRECT' ? ' <span class="sc-muted">direct</span>' : ''}</td><td class="n">${c.count}</td><td class="n">${money(c.value)}</td></tr>`).join('')}</tbody></table></div></div></div>`;
|
||||
return;
|
||||
}
|
||||
const H = { 'order-wise': ['Ref no.', 'Date', 'Type', 'Customer', 'Category', 'Cust PO', 'Qty', 'Sub total', 'Grand total', 'Status', 'SAP SO'],
|
||||
'item-wise': ['Item', 'Description', 'Orders', 'Qty', 'Value', 'Avg. price', 'Special-price lines'],
|
||||
pending: ['Ref no.', 'Order date', 'Age (days)', 'Customer', 'SAP SO', 'Item', 'Description', 'Ordered', 'Delivered', 'Open qty', 'Open value'] }[TAB];
|
||||
if (TAB === 'customer-pending') {
|
||||
o.innerHTML = d.customers.length ? `<div class="card"><div class="tbl-wrap"><table class="lst"><thead><tr><th>Customer</th>${d.items.map(i => `<th class="n" title="${esc(i.description)}">${esc(i.itemCode)}</th>`).join('')}<th class="n">Total qty</th><th class="n">Open value</th></tr></thead><tbody>
|
||||
${d.customers.map(c => `<tr><td>${esc(c.cardName)} <span class="sc-muted">${esc(c.cardCode)}</span></td>${d.items.map(i => `<td class="n">${c.qty[i.itemCode] ? num(c.qty[i.itemCode]) : ''}</td>`).join('')}<td class="n">${num(c.total)}</td><td class="n">${money(c.value)}</td></tr>`).join('')}
|
||||
<tr><td><b>Total</b></td>${d.items.map(i => `<td class="n"><b>${num(i.total)}</b></td>`).join('')}<td></td><td></td></tr></tbody></table></div></div>` : '<div class="empty">Nothing pending.</div>';
|
||||
return;
|
||||
}
|
||||
const rows = rowsFor(d);
|
||||
o.innerHTML = rows.length ? `<div class="card"><div class="card-hd">${rows.length} rows</div><div class="tbl-wrap"><table class="lst"><thead><tr>${H.map((h, i) => `<th class="${typeof rows[0][i] === 'number' ? 'n' : ''}">${h}</th>`).join('')}</tr></thead><tbody>
|
||||
${rows.map(r => `<tr>${r.map(c => `<td class="${typeof c === 'number' ? 'n' : ''}">${typeof c === 'number' ? (Number.isInteger(c) ? num(c) : money(c)) : esc(c)}</td>`).join('')}</tr>`).join('')}</tbody></table></div></div>` : '<div class="empty">No data.</div>';
|
||||
}
|
||||
function rowsFor(d) {
|
||||
if (TAB === 'order-wise') return d.map(r => [r.refNo, date(r.date), r.orderType === 'DIRECT' ? 'Direct' : r.soType === 1 ? 'Trade' : 'Institution', `${r.cardName} (${r.cardCode})`, divName(r.divisionId), r.custOrderNo, r.qty, r.subTotal, r.grandTotal, r.statusLabel, r.sapDocNum ? String(r.sapDocNum) : '']);
|
||||
if (TAB === 'item-wise') return d.map(r => [r.displayCode, r.description, r.orders, r.qty, r.value, r.avgPrice, r.specialLines]);
|
||||
if (TAB === 'pending') return d.map(r => [r.refNo || '', date(r.orderDate), r.ageDays == null ? '' : r.ageDays, `${r.cardName || ''} (${r.cardCode || ''})`, String(r.sapDocNum), r.itemCode, r.description, r.qty, r.deliveredQty, r.openQty, r.openValue]);
|
||||
return [];
|
||||
}
|
||||
function csv() {
|
||||
if (!DATA) return;
|
||||
if (TAB === 'customer-pending') return SC.csv('customer_pending.csv', ['Customer', 'Code', ...DATA.items.map(i => i.itemCode), 'Total', 'Open value'], DATA.customers.map(c => [c.cardName, c.cardCode, ...DATA.items.map(i => c.qty[i.itemCode] || ''), c.total, c.value]));
|
||||
const H = Array.from(document.querySelectorAll('#out th')).map(th => th.textContent);
|
||||
SC.csv(`sales_${TAB}.csv`, H, rowsFor(DATA));
|
||||
}
|
||||
init().catch(e => { q('#app').innerHTML = `<div class="sc-err">${esc(e.message)}</div>`; });
|
||||
</script>
|
||||
<script src="/sidebar.js?v=31"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user