Files
sap-erp/public/customer-portal.html
T
John eead8f5ffd
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s
sale order
2026-10-05 18:45:17 +05:30

302 lines
27 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Customer Portal — Mitra Industries</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;--purple:#9f5afd;--border:#1c2e48;--border2:#243650;--text:#c8d6e8;--text2:#7a9ab8;--text3:#5b7594;--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;-webkit-font-smoothing:antialiased}
.top{display:flex;align-items:center;gap:12px;padding:10px 16px;background:var(--surface3);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:20;flex-wrap:wrap}
.top img{height:30px}.top .brand{font-weight:700;font-size:14px}.top .who{margin-left:auto;text-align:right;font-size:12px}
.nav{display:flex;gap:2px;padding:0 12px;background:var(--surface);border-bottom:1px solid var(--border);overflow-x:auto}
.nav a{padding:10px 14px;font-size:12.5px;font-weight:700;color:var(--text2);text-decoration:none;border-bottom:2px solid transparent;white-space:nowrap}
.nav a.on{color:var(--blue);border-bottom-color:var(--blue)}
main{padding:14px;max-width:1300px;margin:0 auto}
.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:12.5px;font-weight:700;display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--border);flex-wrap:wrap}
.card-bd{padding:12px 14px}
.kpis{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,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:collapse;font-size:12px}
.lst th{text-align:left;padding:8px 10px;font-size:9.5px;font-weight:700;text-transform:uppercase;color:var(--text3);border-bottom:2px solid var(--border2);white-space:nowrap;background:var(--surface3)}
.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)}
.lst tbody tr[data-id]{cursor:pointer}.lst tbody tr[data-id]:hover td{background:rgba(26,111,255,.06)}
.tbl-wrap{overflow-x:auto}
.mono{font-family:var(--mono)}
.empty{text-align:center;padding:26px;color:var(--text3);font-size:12px}
.login{max-width:380px;margin:9vh auto;background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:26px}
.login img{height:42px;display:block;margin:0 auto 10px}.login h1{font-size:17px;text-align:center;margin-bottom:4px}.login p{text-align:center;font-size:12px;color:var(--text3);margin-bottom:18px}
.login .sc-in{margin-bottom:12px;height:38px}
</style>
<link rel="stylesheet" href="/theme.css?v=3"/>
<script src="/sales-common.js?v=4"></script>
</head>
<body>
<div id="root"></div>
<script>
'use strict';
const { esc, money, num, date, dt, q, qa, toast, api, pill } = SC;
// Customer session: sessionStorage + a session cookie (no expiry → cleared when
// the browser closes) so new tabs stay logged in — same model as the ERP login.
const TK = 'cust_token';
function getTok() { try { let t = sessionStorage.getItem(TK); if (!t) { const m = document.cookie.match(/(?:^|;\s*)cust_token=([^;]+)/); if (m) { t = decodeURIComponent(m[1]); sessionStorage.setItem(TK, t); } } return t; } catch (_e) { return null; } }
function setTok(t) { try { sessionStorage.setItem(TK, t); } catch (_e) {} document.cookie = `cust_token=${encodeURIComponent(t)};path=/;SameSite=Lax`; }
function clearTok() { try { sessionStorage.removeItem(TK); } catch (_e) {} document.cookie = 'cust_token=;path=/;max-age=0'; }
SC.base = '/api/sales-portal';
SC.getToken = getTok;
SC.onUnauthorized = () => { clearTok(); showLogin('Your session has expired — please log in again.'); };
let ME = null;
const divName = id => ((ME.allDivisions || ME.divisions).find(d => d.id === id) || {}).name || id; // incl. disabled categories (existing orders)
function showLogin(msg) {
q('#root').innerHTML = `<div class="login"><img src="/logo.png" alt="" onerror="this.style.display='none'"><h1>Customer Portal</h1><p>Mitra Industries Pvt. Ltd. — place and track your orders</p>
${msg ? `<div class="sc-warn">${esc(msg)}</div>` : ''}
<label class="sc-lbl">Customer code</label><input class="sc-in" id="l-c" autocomplete="username" placeholder="e.g. 10ABCDEF">
<label class="sc-lbl">Password</label><input class="sc-in" id="l-p" type="password" autocomplete="current-password">
<button class="sc-btn sc-b-primary" id="l-go" style="width:100%;height:38px">Log in</button>
<p style="margin:14px 0 0">Forgot your password? Please contact your MIPL sales representative.</p></div>`;
const go = async () => {
const b = q('#l-go'); b.disabled = true;
try {
const r = await fetch('/api/sales-portal/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cardCode: q('#l-c').value.trim(), password: q('#l-p').value }) });
const d = await r.json();
if (!r.ok || !d.success) throw new Error(d.message || 'Login failed');
setTok(d.data.token); await boot();
} catch (e) { toast(e.message); b.disabled = false; }
};
q('#l-go').onclick = go; q('#l-p').onkeydown = e => { if (e.key === 'Enter') go(); };
SC.eyeify(q('.login'));
}
async function boot() {
if (!getTok()) return showLogin();
try { ME = await api('/me'); } catch (e) { return; }
q('#root').innerHTML = `<div class="top"><img src="/logo.png" alt="" onerror="this.style.display='none'"><div class="brand">Mitra Industries — Customer Portal</div>
<div class="who"><b>${esc(ME.account.cardName)}</b> (${esc(ME.account.cardCode)})<div class="sc-muted">Wallet: <b id="w-bal">${money(ME.wallet.balance)}</b> · <span class="sc-link" id="lo">Log out</span></div></div></div>
<div class="nav">${[['dashboard', 'Dashboard'], ['new', '+ New Order'], ['orders', 'My Orders'], ['ledger', 'Ledger'], ['password', 'Change Password']].map(([h, l]) => `<a href="#${h}" data-h="${h}">${l}</a>`).join('')}</div>
<main id="view"></main>`;
q('#lo').onclick = () => { clearTok(); ME = null; showLogin(); };
if (ME.account.mustChangePwd && !location.hash.startsWith('#password')) location.hash = '#password';
route();
}
window.addEventListener('hashchange', () => ME && route());
function route() {
const h = (location.hash || '#dashboard').slice(1); const [v, arg] = h.split('/');
qa('.nav a').forEach(a => a.classList.toggle('on', a.dataset.h === v || (v === 'order' && a.dataset.h === 'orders')));
if (ME.account.mustChangePwd && v !== 'password') { location.hash = '#password'; return; }
({ dashboard, new: () => orderForm(), orders, order: () => orderView(arg), modify: () => orderForm(arg), ledger, password }[v] || dashboard)();
}
const view = () => q('#view');
async function refreshWallet() { try { ME.wallet = (await api('/ledger')).summary; q('#w-bal').textContent = money(ME.wallet.balance); } catch (_e) {} }
// ── Dashboard ───────────────────────────────────────────────────────────────
async function dashboard() {
view().innerHTML = '<div class="empty"><span class="sc-spin"></span></div>';
const rows = await api('/orders?limit=500');
const open = rows.filter(o => [1, 2, 3, 4, 5, 6, 7, 8].includes(o.status));
const toPay = rows.filter(o => o.status === 4), toModify = rows.filter(o => o.status === 5);
view().innerHTML = `<div class="kpis">
<div class="kpi"><b>Open orders</b><span>${open.length}</span></div>
<div class="kpi"><b>Awaiting your payment</b><span>${toPay.length}</span><div class="sc-muted">${money(toPay.reduce((a, o) => a + o.grandTotal, 0))}</div></div>
<div class="kpi"><b>Wallet balance</b><span>${money(ME.wallet.balance)}</span>${ME.wallet.pendingCredits ? `<div class="sc-muted">${money(ME.wallet.pendingCredits)} under verification</div>` : ''}</div>
<div class="kpi"><b>Credit limit</b><span>${money(ME.wallet.creditLimit)}</span></div></div>
${toPay.length || toModify.length ? `<div class="card"><div class="card-hd">Needs your action</div><div class="tbl-wrap">${tbl([...toPay, ...toModify])}</div></div>` : ''}
<div class="card"><div class="card-hd">Recent orders <a class="sc-link" href="#orders" style="margin-left:auto">View all</a></div><div class="tbl-wrap">${tbl(rows.slice(0, 10))}</div></div>`;
wireRows();
}
function tbl(rows) {
return rows.length ? `<table class="lst"><thead><tr><th>Ref no.</th><th>Date</th><th>Type</th><th>Category</th><th>Your PO</th><th class="n">Value</th><th>Status</th><th>SAP SO</th></tr></thead><tbody>
${rows.map(o => `<tr data-id="${o.id}"><td class="mono">${esc(o.refNo)}</td><td>${date(o.createdAt)}</td><td>${o.soType === 1 ? 'Trade' : 'Institution'}</td><td>${esc(divName(o.divisionId))}</td>
<td>${esc(o.custOrderNo)}</td><td class="n">${money(o.grandTotal)}</td><td>${pill(o.status, o.customerStatusLabel)}</td><td class="mono">${esc(o.sapDocNum || '')}</td></tr>`).join('')}</tbody></table>`
: '<div class="empty">No orders yet.</div>';
}
function wireRows() { qa('tr[data-id]').forEach(tr => { tr.onclick = () => { location.hash = '#order/' + tr.dataset.id; }; }); }
// ── Orders list ─────────────────────────────────────────────────────────────
async function orders() {
view().innerHTML = `<div class="card"><div class="card-bd" style="display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end">
<div><label class="sc-lbl">Show</label><select class="sc-in" id="o-st"><option value="1,2,3,4,5,6,7,8">Open</option><option value="9">Closed</option><option value="10">Cancelled</option><option value="">All</option></select></div>
<div style="flex:1;min-width:200px"><label class="sc-lbl">Search</label><input class="sc-in" id="o-q" placeholder="Ref no. / your PO / SAP no."></div>
<button class="sc-btn sc-b-primary" id="o-go">Search</button></div></div>
<div class="card"><div class="tbl-wrap" id="o-t"><div class="empty"><span class="sc-spin"></span></div></div></div>`;
const load = async () => { q('#o-t').innerHTML = '<div class="empty"><span class="sc-spin"></span></div>';
const p = new URLSearchParams({ statuses: q('#o-st').value, search: q('#o-q').value.trim() });
try { q('#o-t').innerHTML = tbl(await api('/orders?' + p)); wireRows(); } catch (e) { q('#o-t').innerHTML = `<div class="sc-err">${esc(e.message)}</div>`; } };
q('#o-go').onclick = load; q('#o-q').onkeydown = e => { if (e.key === 'Enter') load(); }; q('#o-st').onchange = load;
load();
}
// ── Order view + customer actions ───────────────────────────────────────────
async function orderView(id) {
view().innerHTML = '<div class="empty"><span class="sc-spin"></span></div>';
let o; try { o = await api('/orders/' + id); } catch (e) { view().innerHTML = `<div class="sc-err">${esc(e.message)}</div>`; return; }
const canCancel = [1, 2, 3, 4, 5].includes(o.status);
view().innerHTML = `<div class="card"><div class="card-hd"><a class="sc-link" href="#orders">← My orders</a> &nbsp; Order ${esc(o.refNo)}
<span style="margin-left:auto;display:flex;gap:8px">
${o.status === 4 ? '<button class="sc-btn sc-b-green" id="x-pay">Make payment</button>' : ''}
${o.status === 5 ? '<button class="sc-btn sc-b-amber" id="x-mod">Modify & resubmit</button>' : ''}
${canCancel ? '<button class="sc-btn sc-b-red" id="x-can">Cancel order</button>' : ''}
<button class="sc-btn sc-b-ghost" id="x-up">Attach document</button></span></div>
<div class="card-bd" id="x-body">${o.status === 4 ? `<div class="sc-info">Your order is approved. Please make the payment of <b>${money(o.grandTotal)}</b> (less any wallet balance) and submit the details here.</div>` : ''}
${o.status === 5 ? '<div class="sc-warn">MIPL has asked you to modify this order — see MIPL remarks below.</div>' : ''}
${SC.orderHtml(o, { employee: false, divisions: ME.allDivisions || ME.divisions })}</div></div>`;
SC.wireOrder(q('#x-body'), o.id);
if (q('#x-pay')) q('#x-pay').onclick = () => payForm(o);
if (q('#x-mod')) q('#x-mod').onclick = () => { location.hash = '#modify/' + o.id; };
if (q('#x-can')) q('#x-can').onclick = () => SC.modal({ title: 'Cancel order ' + esc(o.refNo), body: '<label class="sc-lbl">Reason *</label><textarea class="sc-in" id="c-r"></textarea>',
buttons: [{ label: 'Back' }, { label: 'Cancel order', cls: 'sc-b-red', onClick: async ({ root }) => { await api(`/orders/${o.id}/cancel`, { method: 'POST', body: { remarks: q('#c-r', root).value } }); toast('Order cancelled', 'ok'); refreshWallet(); orderView(o.id); } }] });
q('#x-up').onclick = () => SC.modal({ title: 'Attach document', body: `<label class="sc-lbl">Title</label><input class="sc-in" id="u-t" value="Purchase order copy">
<label class="sc-lbl" style="margin-top:8px">File (PDF / JPG / PNG, max 10 MB)</label><input class="sc-in" type="file" id="u-f" accept=".pdf,.jpg,.jpeg,.png,.bmp">`,
buttons: [{ label: 'Cancel' }, { label: 'Upload', cls: 'sc-b-primary', onClick: async ({ root }) => { const f = q('#u-f', root).files[0]; if (!f) throw new Error('Choose a file');
await SC.upload(f, { entity: 'order', entityId: o.id, docType: 'po_copy', title: q('#u-t', root).value }); toast('Uploaded', 'ok'); orderView(o.id); } }] });
}
function payForm(o) {
const w = o.wallet || ME.wallet;
const calc = (tds) => Math.max(0, Math.round((o.grandTotal - (+tds || 0) - w.balance) * 100) / 100);
const m = SC.modal({ title: 'Payment — order ' + esc(o.refNo), body: `
<div class="sc-grid"><div class="sc-kv"><b>Order value</b><span>${money(o.grandTotal)}</span></div><div class="sc-kv"><b>Wallet balance</b><span>${money(w.balance)}</span></div>
<div class="sc-kv"><b>Payable now</b><span style="font-size:16px;font-weight:700" id="p-due">${money(calc(0))}</span></div></div>
<div class="sc-row" style="margin-top:10px"><div><label class="sc-lbl">TDS deducted by you (₹)</label><input class="sc-in" id="p-tds" type="number" min="0" step="0.01" value="0"></div>
<div><label class="sc-lbl">Amount paid now (₹)</label><input class="sc-in" id="p-amt" type="number" min="0" step="0.01" value="${calc(0)}"></div></div>
<div id="p-det"><div class="sc-row" style="margin-top:8px">
<div><label class="sc-lbl">Payment mode *</label><select class="sc-in" id="p-mode"><option>RTGS</option><option>NEFT</option><option>Cheque</option><option>Online</option></select></div>
<div><label class="sc-lbl" id="p-ref-l">UTR no. *</label><input class="sc-in" id="p-ref"></div>
<div><label class="sc-lbl">Payment date *</label><input class="sc-in" type="date" id="p-date" max="${SC.today()}" value="${SC.today()}"></div></div>
<div style="margin-top:8px"><label class="sc-lbl">Payment proof (PDF / image)</label><input class="sc-in" type="file" id="p-file" accept=".pdf,.jpg,.jpeg,.png,.bmp"></div></div>
<div class="sc-row" style="margin-top:8px"><div><label class="sc-lbl">Payment detail</label><input class="sc-in" id="p-pd" placeholder="e.g. bank name, TDS note"></div></div>
<div style="margin-top:8px"><label class="sc-lbl">Remarks</label><textarea class="sc-in" id="p-rm"></textarea></div>
<div class="sc-muted" style="margin-top:6px">Anything you pay above the payable amount stays in your wallet for future orders. Payments are verified by MIPL Accounts.</div>`,
buttons: [{ label: 'Cancel' }, { label: 'Submit payment', cls: 'sc-b-green', onClick: async ({ root }) => {
const amt = +q('#p-amt', root).value || 0; let docId = null;
const f = q('#p-file', root).files[0];
if (f) docId = (await SC.upload(f, { entity: 'order', entityId: o.id, docType: 'payment', title: 'Payment proof' })).id;
await api(`/orders/${o.id}/pay`, { method: 'POST', body: { paidAmount: amt, tds: q('#p-tds', root).value, paymentMode: q('#p-mode', root).value, paymentRef: q('#p-ref', root).value,
paymentDate: q('#p-date', root).value, paymentDetail: q('#p-pd', root).value, remarks: q('#p-rm', root).value, docId } });
toast('Payment submitted', 'ok'); refreshWallet(); orderView(o.id); } }] });
const R = m.root;
const upd = () => { const d = calc(q('#p-tds', R).value); q('#p-due', R).textContent = money(d); q('#p-det', R).style.display = (+q('#p-amt', R).value > 0) ? '' : 'none'; };
q('#p-tds', R).oninput = () => { q('#p-amt', R).value = calc(q('#p-tds', R).value); upd(); };
q('#p-amt', R).oninput = upd;
q('#p-mode', R).onchange = () => { q('#p-ref-l', R).textContent = ({ Cheque: 'Cheque no. *', Online: 'Online transaction ID *' })[q('#p-mode', R).value] || 'UTR no. *'; };
upd();
}
// ── New order / modify ──────────────────────────────────────────────────────
async function orderForm(modifyId) {
const ex = modifyId ? await api('/orders/' + modifyId) : null;
if (ex && ex.status !== 5) { location.hash = '#order/' + modifyId; return; }
const c = ME.customer;
const addrOpt = (list, sel) => list.map(a => `<option value="${esc(a.code)}" ${a.code === sel ? 'selected' : ''}>${esc(a.code)} — ${esc(a.city)}, ${esc(a.stateName || a.state)}</option>`).join('');
view().innerHTML = `<div class="card"><div class="card-hd">${ex ? `Modify order ${esc(ex.refNo)}` : 'New sales order'}</div><div class="card-bd">
${ex ? `<div class="sc-warn">MIPL remarks:<br>${esc(ex.employeeRemarks || '—')}</div>` : ''}
<div class="sc-row">
<div><label class="sc-lbl">Order type *</label><select class="sc-in" id="n-type" ${ex ? 'disabled' : ''}><option value="1">Trade</option><option value="2">Institution</option></select></div>
<div><label class="sc-lbl">Product category *</label><select class="sc-in" id="n-div" ${ex ? 'disabled' : ''}><option value="">— Select —</option>${ME.divisions.map(d => `<option value="${d.id}">${esc(d.name)}</option>`).join('')}</select></div>
<div><label class="sc-lbl">Your order ref. no. *</label><input class="sc-in" id="n-po" value="${esc(ex ? ex.custOrderNo : '')}">
${ex ? '' : '<div class="sc-muted" id="n-po-h" style="margin-top:3px">Generated automatically — you may type your own PO number instead. <span class="sc-link" id="n-po-reset" style="display:none">Use automatic no.</span></div>'}</div>
<div><label class="sc-lbl">Committed Dispatch Date *</label><input class="sc-in" type="date" id="n-dd" min="${SC.today()}" value="${ex ? SC.iso(ex.deliveryDate) : ''}"></div></div>
<div class="sc-row" style="margin-top:8px">
<div><label class="sc-lbl">Ship to *</label><select class="sc-in" id="n-ship">${addrOpt(c.shipTo, ex && ex.shipToCode)}</select><div class="sc-muted" id="n-ship-t" style="white-space:pre-wrap;margin-top:3px"></div></div>
<div><label class="sc-lbl">Bill to *</label><select class="sc-in" id="n-bill">${addrOpt(c.billTo, ex && ex.billToCode)}</select><div class="sc-muted" id="n-bill-t" style="white-space:pre-wrap;margin-top:3px"></div></div></div>
<div class="sc-sec">Products</div><div id="n-cart" class="sc-muted">Select a product category first.</div>
<div class="sc-row" style="margin-top:10px"><div><label class="sc-lbl">Remarks</label><textarea class="sc-in" id="n-rm"></textarea></div>
${ex ? '' : '<div><label class="sc-lbl">Purchase order copy (PDF / image)</label><input class="sc-in" type="file" id="n-file" accept=".pdf,.jpg,.jpeg,.png,.bmp"></div>'}</div>
<div style="margin-top:12px;text-align:right"><button class="sc-btn sc-b-primary" id="n-save">${ex ? 'Resubmit order' : 'Place order'}</button></div></div></div>`;
if (ex) { q('#n-type').value = ex.soType; q('#n-div').value = ex.divisionId; }
if (!c.shipTo.length || !c.billTo.length) view().insertAdjacentHTML('afterbegin', '<div class="sc-err">No Ship-To / Bill-To address is set up for your account in SAP — please contact MIPL.</div>');
const shipState = () => (c.shipTo.find(a => a.code === q('#n-ship').value) || {}).state;
const addrText = () => { q('#n-ship-t').textContent = (c.shipTo.find(a => a.code === q('#n-ship').value) || {}).text || ''; q('#n-bill-t').textContent = (c.billTo.find(a => a.code === q('#n-bill').value) || {}).text || ''; };
addrText();
// Auto "Your order ref. no." (per customer: CODE/FY/0001) — editable.
let autoNo = '';
if (!ex) {
const po = q('#n-po'), rst = q('#n-po-reset');
const syncHint = () => { rst.style.display = autoNo && po.value.trim() !== autoNo ? '' : 'none'; };
api('/next-order-no').then(d => { autoNo = d.custOrderNo; if (!po.value.trim()) po.value = autoNo; syncHint(); }).catch(() => {});
po.oninput = syncHint;
rst.onclick = () => { po.value = autoNo; syncHint(); };
}
let cart = null;
// Product list = only the selected category's products (server-filtered).
let lastDiv = ex ? String(ex.divisionId) : '';
const loadCart = async () => {
const div = q('#n-div').value;
if (!div) { cart = null; q('#n-cart').className = 'sc-muted'; q('#n-cart').textContent = 'Select a product category first.'; return; }
q('#n-cart').innerHTML = '<span class="sc-spin"></span>';
const prods = await api('/products?divisionId=' + div);
q('#n-cart').className = '';
cart = SC.cartEditor(q('#n-cart'), prods, { lines: ex ? ex.items.map(i => ({ key: i.productId ? `P:${i.productId}` : `S:${i.itemCode}`, qty: i.qty, specialPrice: i.specialPrice })) : [],
allowSpecial: true, settings: ME.settings, getShipState: shipState, categoryName: divName(+div) });
};
q('#n-div').onchange = async () => {
if (cart && cart.count() && !(await SC.confirmBox('Change product category?', 'Products already added belong to the current category and will be removed.', 'Change category', 'sc-b-amber'))) { q('#n-div').value = lastDiv; return; }
lastDiv = q('#n-div').value; loadCart();
};
if (ex) loadCart();
q('#n-ship').onchange = () => { addrText(); cart && cart.refresh(); }; q('#n-bill').onchange = addrText;
q('#n-save').onclick = async (e) => {
if (!cart) return toast('Select a product category and add products');
const body = { soType: +q('#n-type').value, divisionId: +q('#n-div').value, custOrderNo: q('#n-po').value.trim(), deliveryDate: q('#n-dd').value,
// Untouched suggestion → server assigns the next free number at save time.
custOrderNoAuto: !ex && !!autoNo && q('#n-po').value.trim() === autoNo,
shipToCode: q('#n-ship').value, billToCode: q('#n-bill').value, remarks: q('#n-rm').value, lines: cart.getLines() };
e.target.disabled = true;
try {
const o = ex ? await api(`/orders/${ex.id}/resubmit`, { method: 'PUT', body }) : await api('/orders', { method: 'POST', body });
const f = q('#n-file') && q('#n-file').files[0];
if (f) { try { await SC.upload(f, { entity: 'order', entityId: o.id, docType: 'po_copy', title: 'Purchase order copy' }); } catch (er) { toast('Order placed, but the PO copy upload failed: ' + er.message); } }
toast(`Order ${o.refNo} ${ex ? 'resubmitted' : 'placed'} successfully`, 'ok');
location.hash = '#order/' + o.id;
} catch (er) { toast(er.message); e.target.disabled = false; }
};
}
// ── Ledger ──────────────────────────────────────────────────────────────────
async function ledger() {
view().innerHTML = `<div class="card"><div class="card-bd" style="display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end">
<div><label class="sc-lbl">From</label><input class="sc-in" type="date" id="g-f"></div><div><label class="sc-lbl">To</label><input class="sc-in" type="date" id="g-t"></div>
<button class="sc-btn sc-b-primary" id="g-go">Show</button><button class="sc-btn sc-b-ghost" id="g-csv">⬇ Download CSV</button></div></div>
<div class="card"><div class="tbl-wrap" id="g-o"></div></div>`;
let data = null;
const load = async () => { q('#g-o').innerHTML = '<div class="empty"><span class="sc-spin"></span></div>';
data = (await api(`/ledger?from=${q('#g-f').value}&to=${q('#g-t').value}`)).ledger;
q('#g-o').innerHTML = `<table class="lst"><thead><tr><th>Date</th><th>Description</th><th>Order</th><th>Reference</th><th>Status</th><th class="n">Credit</th><th class="n">Debit</th><th class="n">Balance</th></tr></thead><tbody>
<tr><td colspan="7"><i>Opening balance</i></td><td class="n">${money(data.opening)}</td></tr>
${data.lines.map(x => `<tr><td>${dt(x.createdAt)}</td><td>${esc({ topup: 'Payment', order: 'Order', refund: 'Refund', adjustment: 'Adjustment', opening: 'Opening balance' }[x.refType] || x.refType)}</td>
<td>${esc(x.refNo || '')}</td><td>${esc(x.paymentMode)} ${esc(x.paymentRef || x.remarks)}</td><td>${esc(x.status)}</td>
<td class="n">${x.type === 'credit' ? money(x.amount) : ''}</td><td class="n">${x.type === 'debit' ? money(x.amount) : ''}</td><td class="n">${money(x.runningBalance)}</td></tr>`).join('')}
<tr><td colspan="7"><b>Closing balance</b></td><td class="n"><b>${money(data.closing)}</b></td></tr></tbody></table>`; };
q('#g-go').onclick = load;
q('#g-csv').onclick = () => data && SC.csv(`ledger_${ME.account.cardCode}.csv`, ['Date', 'Type', 'Order', 'Mode', 'Reference', 'Status', 'Credit', 'Debit', 'Balance'],
data.lines.map(x => [dt(x.createdAt), x.refType, x.refNo || '', x.paymentMode, x.paymentRef || x.remarks, x.status, x.type === 'credit' ? x.amount : '', x.type === 'debit' ? x.amount : '', x.runningBalance]));
load();
}
// ── Change password ─────────────────────────────────────────────────────────
function password() {
view().innerHTML = `<div class="card" style="max-width:420px"><div class="card-hd">Change password</div><div class="card-bd">
${ME.account.mustChangePwd ? '<div class="sc-warn">Please set a new password before continuing.</div>' : ''}
<label class="sc-lbl">Current password</label><input class="sc-in" type="password" id="w-o" style="margin-bottom:8px">
<label class="sc-lbl">New password (min. 8 characters)</label><input class="sc-in" type="password" id="w-n" style="margin-bottom:8px">
<label class="sc-lbl">Confirm new password</label><input class="sc-in" type="password" id="w-c" style="margin-bottom:12px">
<button class="sc-btn sc-b-primary" id="w-go">Change password</button></div></div>`;
SC.eyeify(view());
q('#w-go').onclick = async () => {
if (q('#w-n').value !== q('#w-c').value) return toast('New passwords do not match');
try { await api('/change-password', { method: 'POST', body: { oldPassword: q('#w-o').value, newPassword: q('#w-n').value } });
toast('Password changed', 'ok'); ME.account.mustChangePwd = false; location.hash = '#dashboard'; } catch (e) { toast(e.message); }
};
}
boot();
</script>
</body>
</html>