/* public/sales-common.js — shared UI helpers for the Sales Order module * (employee pages + customer portal). Exposes window.SC. * Configure before use: SC.base ('/api/sales' or '/api/sales-portal') and * SC.getToken (fn returning the bearer token), SC.onUnauthorized. */ (function () { 'use strict'; const SC = window.SC = {}; SC.base = '/api/sales'; SC.getToken = () => { try { return sessionStorage.getItem('portal_token'); } catch (_e) { return null; } }; SC.onUnauthorized = () => { location.href = '/'; }; // ── styles ──────────────────────────────────────────────────────────────── const css = ` .sc-modal-bg{position:fixed;inset:0;background:rgba(3,8,16,.62);z-index:900;display:flex;align-items:flex-start;justify-content:center;overflow-y:auto;padding:28px 10px} .sc-modal{background:var(--surface,#0f1d30);border:1px solid var(--border2,#243650);border-radius:12px;width:100%;max-width:760px;box-shadow:0 18px 60px rgba(0,0,0,.55);color:var(--text,#c8d6e8)} .sc-modal.wide{max-width:1180px} .sc-mh{display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid var(--border,#1c2e48);font-weight:700;font-size:14px} .sc-mh .x{margin-left:auto;background:none;border:none;color:var(--text2,#7a9ab8);font-size:20px;cursor:pointer;line-height:1} .sc-mb{padding:14px 16px} .sc-mf{display:flex;gap:8px;justify-content:flex-end;padding:12px 16px;border-top:1px solid var(--border,#1c2e48);flex-wrap:wrap} .sc-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:10px} .sc-kv{font-size:12px}.sc-kv b{display:block;font-size:9.5px;text-transform:uppercase;letter-spacing:.4px;color:var(--text3,#3d5470);margin-bottom:2px;font-weight:700} .sc-kv span{white-space:pre-wrap} .sc-sec{margin-top:14px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--text2,#7a9ab8);border-bottom:1px solid var(--border,#1c2e48);padding-bottom:5px;margin-bottom:8px} .sc-tbl{width:100%;border-collapse:collapse;font-size:12px} .sc-tbl th{text-align:left;padding:6px 8px;font-size:9.5px;text-transform:uppercase;letter-spacing:.3px;color:var(--text3,#3d5470);border-bottom:2px solid var(--border2,#243650);white-space:nowrap} .sc-tbl td{padding:6px 8px;border-bottom:1px solid var(--border,#1c2e48);vertical-align:top} .sc-tbl td.n,.sc-tbl th.n{text-align:right;font-family:var(--mono,monospace)} .sc-tbl tr.tot td{font-weight:700;border-top:2px solid var(--border2,#243650)} .sc-pill{display:inline-block;font-size:10px;font-weight:700;padding:2px 9px;border-radius:999px;white-space:nowrap} .sc-in{width:100%;padding:7px 9px;border:1.5px solid var(--border2,#243650);border-radius:7px;font-size:12.5px;background:var(--surface2,#142338);color:var(--text,#c8d6e8);font-family:inherit;outline:none} .sc-in:focus{border-color:var(--blue,#1a6fff)} textarea.sc-in{min-height:58px;resize:vertical} .sc-lbl{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:var(--text3,#3d5470);margin-bottom:3px;display:block} .sc-btn{padding:7px 14px;border-radius:8px;border:none;font-size:12px;font-weight:700;cursor:pointer;font-family:inherit} .sc-btn:disabled{opacity:.5;cursor:not-allowed} .sc-b-primary{background:var(--blue,#1a6fff);color:#fff}.sc-b-green{background:var(--green,#0dbb8a);color:#fff} .sc-b-red{background:var(--red,#e53e3e);color:#fff}.sc-b-amber{background:var(--amber,#f5a623);color:#111} .sc-b-ghost{background:var(--surface2,#142338);color:var(--text2,#7a9ab8);border:1px solid var(--border2,#243650)} .sc-remarks{white-space:pre-wrap;font-size:12px;background:var(--surface3,#0b1520);border:1px solid var(--border,#1c2e48);border-radius:8px;padding:8px 10px;max-height:160px;overflow:auto} .sc-warn{background:rgba(245,166,35,.12);border:1px solid rgba(245,166,35,.4);color:var(--amber,#f5a623);border-radius:8px;padding:8px 10px;font-size:12px;margin:8px 0} .sc-info{background:rgba(26,111,255,.1);border:1px solid rgba(26,111,255,.35);border-radius:8px;padding:8px 10px;font-size:12px;margin:8px 0} .sc-err{background:rgba(229,62,62,.12);border:1px solid rgba(229,62,62,.4);color:#ff8a8a;border-radius:8px;padding:8px 10px;font-size:12px;margin:8px 0;white-space:pre-wrap} .sc-diff{background:rgba(229,62,62,.14)!important} .sc-toast-c{position:fixed;top:14px;right:10px;z-index:2000;display:flex;flex-direction:column;gap:6px} .sc-toast{padding:9px 13px;border-radius:8px;font-size:12px;font-weight:600;box-shadow:0 8px 24px rgba(0,0,0,.4);max-width:420px} .sc-t-err{background:#e53e3e;color:#fff}.sc-t-ok{background:#0dbb8a;color:#fff} .sc-row{display:flex;gap:10px;flex-wrap:wrap}.sc-row>*{flex:1;min-width:160px} .sc-link{color:var(--blue,#1a6fff);cursor:pointer;font-weight:600;text-decoration:none}.sc-link:hover{text-decoration:underline} .sc-muted{color:var(--text3,#3d5470);font-size:11px} .sc-ps{position:relative} .sc-ps-list{position:absolute;z-index:60;left:0;right:0;top:100%;margin-top:2px;background:var(--surface2,#142338);border:1px solid var(--border2,#243650);border-radius:8px;max-height:300px;overflow:auto;box-shadow:0 10px 30px rgba(0,0,0,.45)} .sc-ps-list div{padding:7px 10px;font-size:12px;cursor:pointer;border-bottom:1px solid var(--border,#1c2e48);display:flex;gap:6px;align-items:baseline} .sc-ps-list div.on,.sc-ps-list div:hover{background:rgba(26,111,255,.14)} .sc-ps-price{margin-left:auto;white-space:nowrap;color:var(--text2,#7a9ab8);font-family:var(--mono,monospace);font-size:11px} .sc-tl{font-size:11.5px}.sc-tl div{padding:4px 0;border-bottom:1px dashed var(--border,#1c2e48)} .sc-spin{width:14px;height:14px;border:2px solid rgba(26,111,255,.25);border-top-color:#1a6fff;border-radius:50%;animation:scspin .6s linear infinite;display:inline-block;vertical-align:middle} @keyframes scspin{to{transform:rotate(360deg)}} @media (max-width:640px){.sc-modal-bg{padding:0}.sc-modal{border-radius:0;min-height:100vh}.sc-tbl{font-size:11px}}`; const st = document.createElement('style'); st.textContent = css; document.head.appendChild(st); // ── basics ──────────────────────────────────────────────────────────────── SC.esc = v => (v == null ? '' : String(v)).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); SC.money = v => (v == null || v === '' ? '—' : '₹ ' + Number(v).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })); SC.num = v => (v == null || v === '' ? '' : Number(v).toLocaleString('en-IN', { maximumFractionDigits: 3 })); const p2 = n => String(n).padStart(2, '0'); SC.date = v => { if (!v) return '—'; const d = new Date(v); return isNaN(d) ? '—' : `${p2(d.getDate())}-${p2(d.getMonth() + 1)}-${d.getFullYear()}`; }; SC.dt = v => { if (!v) return '—'; const d = new Date(v); return isNaN(d) ? '—' : `${SC.date(d)} ${p2(d.getHours())}:${p2(d.getMinutes())}`; }; SC.iso = v => { if (!v) return ''; const d = new Date(v); return isNaN(d) ? '' : `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())}`; }; SC.today = () => SC.iso(new Date()); SC.q = (s, root) => (root || document).querySelector(s); SC.qa = (s, root) => Array.from((root || document).querySelectorAll(s)); SC.toast = (m, kind) => { let c = SC.q('.sc-toast-c'); if (!c) { c = document.createElement('div'); c.className = 'sc-toast-c'; document.body.appendChild(c); } const e = document.createElement('div'); e.className = 'sc-toast ' + (kind === 'ok' ? 'sc-t-ok' : 'sc-t-err'); e.textContent = m; c.appendChild(e); setTimeout(() => e.remove(), kind === 'ok' ? 3500 : 6500); }; SC.api = async (p, opts = {}) => { const headers = { Authorization: 'Bearer ' + SC.getToken() }; const o = { method: opts.method || 'GET', headers }; if (opts.form) o.body = opts.form; else if (opts.body !== undefined) { headers['Content-Type'] = 'application/json'; o.body = JSON.stringify(opts.body); } const r = await fetch(SC.base + p, o); if (r.status === 401) { SC.onUnauthorized(); throw new Error('Session expired'); } const d = await r.json().catch(() => ({ success: false, message: 'HTTP ' + r.status })); if (!r.ok || d.success === false) throw new Error(d.message || ('HTTP ' + r.status)); return d.data; }; // Open an authenticated file (doc / COA / invoice PDF) in a new tab. SC.openFile = async (p) => { const w = window.open('', '_blank'); if (w) w.document.write('

Loading…

'); try { const r = await fetch(SC.base + p, { headers: { Authorization: 'Bearer ' + SC.getToken() } }); if (!r.ok) { const d = await r.json().catch(() => ({})); throw new Error(d.message || 'HTTP ' + r.status); } const url = URL.createObjectURL(await r.blob()); if (w) w.location.href = url; else window.open(url, '_blank'); } catch (e) { if (w) w.close(); SC.toast(e.message); } }; SC.upload = async (file, fields) => { const fd = new FormData(); fd.append('file', file); Object.entries(fields).forEach(([k, v]) => fd.append(k, v)); return SC.api('/docs', { method: 'POST', form: fd }); }; SC.csv = (filename, headers, rows) => { const cell = v => { const s = v == null ? '' : String(v); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; }; const text = [headers.map(cell).join(','), ...rows.map(r => r.map(cell).join(','))].join('\r\n'); const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob(['' + text], { type: 'text/csv' })); a.download = filename; a.click(); }; // ── show/hide password (eye button) ─────────────────────────────────────── const EYE = ''; const EYE_OFF = ''; const eyeCss = document.createElement('style'); eyeCss.textContent = `.sc-pw{position:relative;display:block}.sc-pw>input{padding-right:42px!important} .sc-pw-eye{position:absolute;right:5px;top:50%;transform:translateY(-50%);width:32px;height:30px;border:none;background:transparent;cursor:pointer;color:var(--text2,#7a9ab8);display:flex;align-items:center;justify-content:center;border-radius:6px} .sc-pw-eye:hover{color:var(--blue,#1a6fff);background:rgba(26,111,255,.1)}`; document.head.appendChild(eyeCss); // Wrap each password input in `root` with an eye toggle (idempotent). SC.eyeify = (root) => { SC.qa('input[type="password"]', root).forEach(inp => { if (inp.parentElement.classList.contains('sc-pw')) return; // Move the input's bottom margin onto the wrapper so the eye stays vertically centred on the field. const mb = getComputedStyle(inp).marginBottom; inp.style.marginBottom = '0'; const w = document.createElement('span'); w.className = 'sc-pw'; if (mb && mb !== '0px') w.style.marginBottom = mb; inp.parentNode.insertBefore(w, inp); w.appendChild(inp); const b = document.createElement('button'); b.type = 'button'; b.className = 'sc-pw-eye'; b.title = 'Show password'; b.setAttribute('aria-label', 'Show password'); b.innerHTML = EYE; b.addEventListener('mousedown', e => e.preventDefault()); b.addEventListener('click', () => { const show = inp.type === 'password'; inp.type = show ? 'text' : 'password'; b.innerHTML = show ? EYE_OFF : EYE; b.title = show ? 'Hide password' : 'Show password'; b.setAttribute('aria-label', b.title); inp.focus(); }); w.appendChild(b); }); }; // ── status pills ────────────────────────────────────────────────────────── const COLORS = { 1: '#f5a623', 2: '#06b6d4', 3: '#06b6d4', 4: '#9f5afd', 5: '#ec4899', 6: '#f5a623', 7: '#1a6fff', 8: '#0dbb8a', 9: '#64748b', 10: '#e53e3e', 11: '#e53e3e' }; SC.pill = (status, label) => { const c = COLORS[status] || '#64748b'; return `${SC.esc(label)}`; }; SC.typeLabel = o => (o.orderType === 'DIRECT' ? 'Direct' : o.soType === 1 ? 'Trade' : 'Institution'); // ── modal (closes ONLY via its buttons — never on backdrop click) ───────── SC.modal = ({ title, body, wide, buttons = [] }) => { const bg = document.createElement('div'); bg.className = 'sc-modal-bg'; bg.innerHTML = `
${title}
${body}
${buttons.length ? '
' : ''}
`; document.body.appendChild(bg); document.body.style.overflow = 'hidden'; const close = () => { bg.remove(); if (!SC.q('.sc-modal-bg')) document.body.style.overflow = ''; }; SC.q('.x', bg).onclick = close; const f = SC.q('.sc-mf', bg); buttons.forEach(b => { const el = document.createElement('button'); el.className = 'sc-btn ' + (b.cls || 'sc-b-ghost'); el.textContent = b.label; el.onclick = async () => { if (!b.onClick) return close(); el.disabled = true; const old = el.textContent; el.innerHTML = ' ' + SC.esc(old); try { const keep = await b.onClick({ close, root: bg }); if (keep !== true) close(); } catch (e) { SC.toast(e.message); } finally { el.disabled = false; el.textContent = old; } }; f.appendChild(el); }); return { root: bg, close }; }; SC.confirmBox = (title, html, okLabel, okCls) => new Promise(res => { SC.modal({ title, body: html, buttons: [{ label: 'Back', onClick: () => res(false) }, { label: okLabel || 'Confirm', cls: okCls || 'sc-b-primary', onClick: () => res(true) }] }); }); // ── tax/total preview (server recomputes authoritatively) ──────────────── SC.taxFor = (settings, shipState) => { const intra = String(shipState || '').toUpperCase() === String(settings.companyState || '').toUpperCase(); return intra ? { taxCode: settings.intraTaxCode, igst: 0, cgst: +settings.cgstRate || 0, sgst: +settings.sgstRate || 0, tcs: +settings.tcsRate || 0 } : { taxCode: settings.interTaxCode, igst: +settings.igstRate || 0, cgst: 0, sgst: 0, tcs: +settings.tcsRate || 0 }; }; const r2 = n => Math.round((+n || 0) * 100) / 100; SC.totals = (lines, tax) => { const sub = r2(lines.reduce((a, l) => a + l.lineTotal, 0)); const igst = r2(sub * tax.igst / 100), cgst = r2(sub * tax.cgst / 100), sgst = r2(sub * tax.sgst / 100); const tcs = tax.tcs > 0 ? r2((sub + igst + cgst + sgst) * tax.tcs / 100) : 0; return { sub, igst, cgst, sgst, tcs, grand: r2(sub + igst + cgst + sgst + tcs) }; }; SC.totalsRows = (t, tax, colspan) => ` Total before taxes${SC.money(t.sub)} ${tax.igst ? `IGST @ ${tax.igst}%${SC.money(t.igst)}` : ''} ${tax.cgst ? `CGST @ ${tax.cgst}%${SC.money(t.cgst)}` : ''} ${tax.sgst ? `SGST @ ${tax.sgst}%${SC.money(t.sgst)}` : ''} ${t.tcs ? `TCS @ ${tax.tcs}%${SC.money(t.tcs)}` : ''} Grand total${SC.money(t.grand)}`; // ── cart editor ─────────────────────────────────────────────────────────── // container: element; products: [{key,displayCode,description,price,packSize,hsn}] // opts: { lines:[{key,qty,specialPrice,commission}], allowSpecial, allowCommission, settings, getShipState } SC.cartEditor = (container, products, opts = {}) => { const st = { lines: (opts.lines || []).map(l => ({ ...l })) }; const byKey = Object.fromEntries(products.map(p => [p.key, p])); const compute = () => st.lines.map(l => { const p = byKey[l.key] || {}; const eff = +l.specialPrice > 0 ? +l.specialPrice : +p.price || 0; return { ...l, p, nop: Math.max(1, Math.ceil((+l.qty || 0) / (p.packSize || 1))), lineTotal: r2((+l.qty || 0) * eff), diff: +l.specialPrice > 0 && Math.abs(+l.specialPrice - (+p.price || 0)) > 0.0001 }; }); // Searchable product picker: matches every typed word against item code, // display code and description; ↑/↓ to move, Enter to pick, Esc to close. function wirePicker() { const qi = SC.q('.sc-prod-q', container), hid = SC.q('.sc-prod', container), list = SC.q('.sc-ps-list', container); if (!qi) return; let hits = [], hi = 0; const label = p => `${p.displayCode} — ${p.description}`; const draw = () => { const words = qi.value.toLowerCase().split(/\s+/).filter(Boolean); hits = products.filter(p => { const s = `${p.displayCode} ${p.itemCode} ${p.description} ${p.shortDesc || ''}`.toLowerCase(); return words.every(w => s.includes(w)); }).slice(0, 60); hi = Math.min(hi, Math.max(0, hits.length - 1)); list.innerHTML = hits.length ? hits.map((p, i) => `
${SC.esc(p.displayCode)} — ${SC.esc(p.description)} ${SC.money(p.price)}${st.lines.some(l => l.key === p.key) ? ' · in cart' : ''}
`).join('') : '
No matching product in this category
'; list.style.display = 'block'; SC.qa('[data-k]', list).forEach(el => { el.onmousedown = e => { e.preventDefault(); pick(el.dataset.k); }; }); }; const pick = key => { const p = byKey[key]; if (!p) return; hid.value = key; qi.value = label(p); list.style.display = 'none'; const qq = SC.q('.sc-qty', container); qq.focus(); qq.select(); }; qi.oninput = () => { hid.value = ''; hi = 0; draw(); }; qi.onfocus = () => { if (!hid.value) draw(); }; qi.onblur = () => setTimeout(() => { list.style.display = 'none'; }, 120); qi.onkeydown = e => { if (list.style.display === 'none' && ['ArrowDown', 'ArrowUp'].includes(e.key)) { draw(); return; } if (e.key === 'ArrowDown') { hi = Math.min(hi + 1, hits.length - 1); draw(); e.preventDefault(); } else if (e.key === 'ArrowUp') { hi = Math.max(hi - 1, 0); draw(); e.preventDefault(); } else if (e.key === 'Enter') { if (hits[hi]) pick(hits[hi].key); e.preventDefault(); } else if (e.key === 'Escape') list.style.display = 'none'; }; SC.q('.sc-qty', container).onkeydown = e => { if (e.key === 'Enter') { e.preventDefault(); SC.q('.sc-add', container).click(); } }; } function render() { const ls = compute(); const tax = SC.taxFor(opts.settings || {}, opts.getShipState ? opts.getShipState() : ''); const t = SC.totals(ls, tax); const cols = 6 + (opts.allowSpecial ? 1 : 0) + (opts.allowCommission ? 1 : 0); container.innerHTML = `
${opts.allowSpecial ? '' : ''}${opts.allowCommission ? '' : ''} ${ls.length ? ls.map((l, i) => ` ${opts.allowSpecial ? `` : ''} ${opts.allowCommission ? `` : ''} `).join('') : ``} ${ls.length ? SC.totalsRows(t, tax, cols) + '' : ''}
#ItemDescriptionQtyPacksPriceSpecial priceComm %Amount
${i + 1}${SC.esc(l.p.displayCode || l.key)}${SC.esc(l.p.description || '')}
HSN ${SC.esc(l.p.hsn || '—')}
${l.nop}${SC.money(l.p.price)}${SC.money(l.lineTotal)}
No products added yet
${ls.some(l => l.diff) ? '
A special price differs from the list price — the order will be reviewed by MIPL before approval.
' : ''}`; wirePicker(); SC.q('.sc-add', container).onclick = () => { const key = SC.q('.sc-prod', container).value; const qty = +SC.q('.sc-qty', container).value; if (!key) return SC.toast('Search and select a product from the list'); if (!(qty > 0)) return SC.toast('Enter a quantity'); const ex = st.lines.find(l => l.key === key); if (ex) ex.qty = (+ex.qty || 0) + qty; else st.lines.push({ key, qty, specialPrice: 0, commission: 0 }); render(); opts.onChange && opts.onChange(); }; SC.qa('tr[data-i]', container).forEach(tr => { const i = +tr.dataset.i; const upd = (cls, f) => { const el = SC.q(cls, tr); if (el) el.onchange = () => { st.lines[i][f] = el.value === '' ? 0 : +el.value; render(); opts.onChange && opts.onChange(); }; }; upd('.sc-lq', 'qty'); upd('.sc-lsp', 'specialPrice'); upd('.sc-lcm', 'commission'); SC.q('.sc-del', tr).onclick = () => { st.lines.splice(i, 1); render(); opts.onChange && opts.onChange(); }; }); } render(); return { count: () => st.lines.length, getLines: () => st.lines.filter(l => +l.qty > 0).map(l => ({ key: l.key, qty: +l.qty, specialPrice: +l.specialPrice || 0, commission: +l.commission || 0 })), computed: compute, refresh: render }; }; // ── order detail view ───────────────────────────────────────────────────── SC.orderHtml = (o, { employee, divisions, labels } = {}) => { const divName = ((divisions || []).find(d => d.id === o.divisionId) || {}).name || o.divisionId; const tax = { igst: o.igstRate, cgst: o.cgstRate, sgst: o.sgstRate, tcs: o.tcsRate }; const t = { sub: o.subTotal, igst: o.igstVal, cgst: o.cgstVal, sgst: o.sgstVal, tcs: o.tcsVal, grand: o.grandTotal }; const showSp = o.items.some(i => i.specialPrice > 0); const showCm = employee && o.items.some(i => i.commission > 0); const cols = 6 + (showSp ? 1 : 0) + (showCm ? 1 : 0); return `
Order ref.${SC.esc(o.refNo)}
Status${SC.pill(o.status, employee ? o.statusLabel : o.customerStatusLabel)}
Type${SC.typeLabel(o)}${o.legacy ? ' · migrated from msale' : ''}
Product category${SC.esc(divName)}
Customer${SC.esc(o.cardName)} (${SC.esc(o.cardCode)})
Customer ref. order no.${SC.esc(o.custOrderNo || '—')}
Order date${SC.date(o.orderDate || o.createdAt)}
Committed Dispatch Date${SC.date(o.deliveryDate)}
Contact person${SC.esc(o.contactPerson || '—')}
Sales employee${SC.esc(o.salesEmployee || '—')}
${o.creditDays != null ? `
Credit days${SC.esc(o.creditDays)}
` : ''} ${o.sapDocNum ? `
SAP sales order${SC.esc(o.sapDocNum)} · ${SC.date(o.sapOrderDate || o.sapPostedAt)}
` : ''}
Created by${SC.esc(o.createdByName)} · ${SC.dt(o.createdAt)}
Ship to — ${SC.esc(o.shipToCode)}${SC.esc(o.shipToText)}
Bill to — ${SC.esc(o.billToCode)}${SC.esc(o.billToText)}
${o.sapError && employee ? `
Last SAP posting error: ${SC.esc(o.sapError)}
` : ''}
Items
${showSp ? '' : ''}${showCm ? '' : ''} ${o.items.map((i, n) => `${showSp ? `` : ''} ${showCm ? `` : ''}`).join('')} ${SC.totalsRows(t, tax, cols)}
#ItemDescriptionQtyPacksPriceSpecial priceComm %Amount
${n + 1}${SC.esc(i.displayCode)}${i.displayCode !== i.itemCode ? `
SAP ${SC.esc(i.itemCode)}
` : ''}
${SC.esc(i.description)}
HSN ${SC.esc(i.hsn || '—')}
${SC.num(i.qty)}${SC.esc(i.nop || '')} ${SC.money(i.price)}${i.specialPrice > 0 ? SC.money(i.specialPrice) : '—'}${i.commission || ''}${SC.money(i.lineTotal)}
${o.consignees && o.consignees.length ? `
Consignees
${o.consignees.map(c => ``).join('')}
Ship toSales emp.ItemsSAP SO
${SC.esc(c.shipToCode)}
${SC.esc(c.shipToText)}
${SC.esc(c.seName)}
${SC.esc(c.seEmail)}
${c.items.map(x => `${SC.esc(x.itemCode)} × ${SC.num(x.qty)}`).join('
')}
${SC.esc(c.sapDocNum || '—')}
` : ''} ${o.paidAmount != null || o.payments && o.payments.length ? `
Payment
Paid amount${SC.money(o.paidAmount)}
TDS${SC.money(o.tdsVal)}
Adjusted from wallet${SC.money(o.walletUsed)}
${o.rebate ? `
Rounding rebate${SC.money(o.rebate)}
` : ''}
Mode / ref.${SC.esc(o.paymentMode || '—')} ${SC.esc(o.paymentRef || '')}
Payment date${SC.date(o.paymentDate)}
Payment status${SC.esc(o.paidStatus || '—')}
${(o.payments || []).length ? ` ${o.payments.map(p => ``).join('')}
TxnTypeAmountStatusRefWhen
#${p.id}${SC.esc(p.refType)} (${SC.esc(p.type)})${SC.money(p.amount)}${SC.esc(p.status)}${SC.esc(p.paymentRef || p.remarks || '')}${SC.dt(p.createdAt)}
` : ''}` : ''}
Remarks
${SC.esc(o.customerRemarks || '—')}
${SC.esc(o.employeeRemarks || '—')}
${employee ? `
${SC.esc(o.internalRemarks || '—')}
` : ''}
${(o.docs || []).length ? `
Documents
${o.docs.map(d => `
📄 ${SC.esc(d.title || d.origName || d.fileName)} ${SC.esc(d.docType)} · ${SC.esc(d.createdByName)} · ${SC.dt(d.createdAt)}
`).join('')}` : ''}
History
${(o.log || []).map(l => `
${SC.dt(l.at)} — ${SC.esc(l.actorName)}: ${SC.esc(l.action.replace(/_/g, ' '))}${l.toStatus != null && l.toStatus !== l.fromStatus ? ` → ${SC.esc((labels || {})[l.toStatus] || l.toStatus)}` : ''}${l.remarks ? ` — ${SC.esc(l.remarks)}` : ''}
`).join('') || '—'}
Invoices, dispatch & COA
${[8, 9].includes(o.status) ? 'Load from SAP' : 'Available once the order is placed in SAP.'}
`; }; // Wire the doc links + invoice loader inside a rendered order view. SC.wireOrder = (root, orderId) => { SC.qa('[data-doc]', root).forEach(el => { el.onclick = () => SC.openFile(`/docs/${el.dataset.doc}`); }); const box = SC.q('.sc-inv', root); const ld = box && SC.q('.sc-inv-load', box); if (ld) ld.onclick = () => SC.loadInvoices(box, orderId); }; SC.loadInvoices = async (box, orderId) => { box.innerHTML = ' Loading from SAP…'; try { const d = await SC.api(`/orders/${orderId}/invoices`); if (!d.invoices.length) { box.innerHTML = 'No invoice raised yet.'; return; } box.innerHTML = d.invoices.map(inv => `
Invoice ${SC.esc(inv.invoiceNo)} · ${SC.date(inv.invoiceDate)} · ${SC.money(inv.total)}
${inv.hasPdf ? `📄 Invoice PDF` : 'Invoice PDF not uploaded yet'}
Dispatch — LR/GR/AWB: ${SC.esc(inv.dispatch.lrNo || '—')}${inv.dispatch.lrDate ? ' · ' + SC.date(inv.dispatch.lrDate) : ''}${inv.dispatch.transporter ? ' · ' + SC.esc(inv.dispatch.transporter) : ''}${inv.dispatch.vehicle ? ' · ' + SC.esc(inv.dispatch.vehicle) : ''}${inv.dispatch.ewayBill ? ' · e-Way ' + SC.esc(inv.dispatch.ewayBill) : ''}
${inv.attachments.length ? `
${inv.attachments.map(a => `📎 ${SC.esc(a.fileName)}.${SC.esc(a.ext)}`).join('')}
` : ''} ${inv.lines.map(l => { const bs = inv.batches.filter(b => b.itemCode === l.itemCode); return ``; }).join('')}
ItemQtyBatches / COA
${SC.esc(l.itemCode)}
${SC.esc(l.description)}
${SC.num(l.qty)} ${bs.map(b => `${SC.esc(b.batch)} (exp ${SC.date(b.expDate)}) ${b.coa ? `COA` : 'no COA'}`).join('
') || '—'}
`).join(''); SC.qa('[data-pdf]', box).forEach(el => { el.onclick = () => SC.openFile(`/invoice-pdf/${el.dataset.pdf}?orderId=${orderId}`); }); SC.qa('[data-att]', box).forEach(el => { el.onclick = () => SC.openFile(`/attachment/${el.dataset.att}?orderId=${orderId}`); }); } catch (e) { box.innerHTML = `
${SC.esc(e.message)}
`; } }; })();