911 lines
44 KiB
HTML
911 lines
44 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<script src="/auth-guard.js?v=4"></script>
|
||
<script src="/company-list.js"></script>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Purchase Order — SAP B1</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Mono:wght@300;400;500&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
--bg:#f5f3ef;--surface:#fff;--surface2:#f9f8f5;
|
||
--border:#e2ddd5;--border2:#ccc7bb;
|
||
--accent:#1a1a1a;--accent2:#c8793a;--accent3:#2d6a4f;
|
||
--text:#1a1a1a;--text-muted:#7a746c;--text-label:#4a4540;
|
||
--danger:#c0392b;--success:#2d6a4f;--warning:#c8793a;
|
||
--shadow:0 1px 3px rgba(0,0,0,.06),0 4px 16px rgba(0,0,0,.04);
|
||
--shadow-lg:0 8px 40px rgba(0,0,0,.14);--r:8px;
|
||
}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{background:var(--bg);font-family:'DM Sans',sans-serif;color:var(--text);min-height:100vh;font-size:13px;}
|
||
|
||
/* ── TOP BAR ── */
|
||
.topbar{background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:space-between;padding:0 20px;height:50px;position:sticky;top:0;z-index:300;}
|
||
.tb-brand{display:flex;align-items:center;gap:10px;}
|
||
.tb-mark{width:28px;height:28px;background:var(--accent2);border-radius:5px;display:flex;align-items:center;justify-content:center;font-family:'Syne',sans-serif;font-weight:800;font-size:13px;color:#fff;letter-spacing:-.5px;}
|
||
.tb-title{font-family:'Syne',sans-serif;font-weight:700;font-size:13px;}
|
||
.tb-sub{font-family:'DM Mono',monospace;font-size:9px;color:rgba(255,255,255,.4);margin-top:1px;}
|
||
.tb-actions{display:flex;gap:6px;align-items:center;}
|
||
.user-chip{font-family:'DM Mono',monospace;font-size:9px;color:rgba(255,255,255,.4);margin-left:4px;}
|
||
|
||
/* ── BUTTONS ── */
|
||
.btn{display:inline-flex;align-items:center;gap:5px;padding:6px 13px;border-radius:5px;font-size:11px;font-weight:600;cursor:pointer;border:none;transition:all .15s;font-family:'DM Sans',sans-serif;white-space:nowrap;}
|
||
.btn-primary{background:var(--accent2);color:#fff;}.btn-primary:hover{background:#b5692d;}
|
||
.btn-success{background:var(--accent3);color:#fff;}.btn-success:hover{background:#245a42;}
|
||
.btn-ghost{background:transparent;color:rgba(255,255,255,.7);border:1px solid rgba(255,255,255,.2);}.btn-ghost:hover{color:#fff;border-color:rgba(255,255,255,.5);}
|
||
.btn-outline{background:transparent;color:var(--text-label);border:1px solid var(--border2);}.btn-outline:hover{border-color:var(--accent2);color:var(--accent2);}
|
||
.btn-sm{padding:4px 10px;font-size:10px;}
|
||
.btn:disabled{opacity:.5;cursor:not-allowed;}
|
||
|
||
/* ── BADGES ── */
|
||
.badge{padding:3px 8px;border-radius:20px;font-size:9px;font-weight:700;font-family:'DM Mono',monospace;letter-spacing:.07em;text-transform:uppercase;}
|
||
.badge-draft{background:rgba(255,255,255,.1);color:rgba(255,255,255,.6);border:1px solid rgba(255,255,255,.15);}
|
||
.badge-open{background:#edf7f1;color:var(--success);border:1px solid #a8d5bc;}
|
||
|
||
/* ── PAGE TABS ── */
|
||
.page-tabs{display:flex;gap:0;border-bottom:1px solid var(--border);background:var(--surface);position:sticky;top:50px;z-index:200;padding:0 18px;}
|
||
.ptab{padding:10px 18px;font-size:11px;font-weight:600;color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:all .12s;user-select:none;font-family:'Syne',sans-serif;letter-spacing:.04em;}
|
||
.ptab.active{color:var(--accent2);border-bottom-color:var(--accent2);}
|
||
.ptab:hover:not(.active){color:var(--text);}
|
||
.ptab-pane{display:none;}
|
||
.ptab-pane.active{display:block;}
|
||
|
||
/* ── MAIN LAYOUT ── */
|
||
.main{max-width:100%;padding:14px 18px 80px;}
|
||
|
||
/* ── FORM ELEMENTS ── */
|
||
.field{display:flex;flex-direction:column;gap:3px;}
|
||
.field label{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.09em;color:var(--text-muted);}
|
||
.req{color:var(--accent2);}
|
||
input,select,textarea{background:var(--surface);border:1.5px solid var(--border);border-radius:5px;color:var(--text);font-family:'DM Sans',sans-serif;font-size:12px;padding:5px 8px;width:100%;outline:none;transition:border-color .15s;}
|
||
input:focus,select:focus,textarea:focus{border-color:var(--accent2);box-shadow:0 0 0 2px rgba(200,121,58,.1);}
|
||
input[readonly]{background:var(--surface2);color:var(--text-muted);cursor:default;}
|
||
select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%237a746c'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 7px center;padding-right:22px;cursor:pointer;appearance:none;}
|
||
textarea{resize:vertical;min-height:56px;}
|
||
|
||
/* ── GRIDS ── */
|
||
.g2{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
|
||
.g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;}
|
||
.g4{display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:8px;}
|
||
.s2{grid-column:span 2;}
|
||
|
||
/* ── HEADER CARD ── */
|
||
.hcard{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);box-shadow:var(--shadow);padding:14px 16px;margin-bottom:10px;position:relative;z-index:10;}
|
||
.hcard-row{display:grid;grid-template-columns:1fr auto;gap:16px;align-items:start;margin-bottom:10px;}
|
||
|
||
/* ── META STRIP ── */
|
||
.meta-strip{display:grid;grid-template-columns:repeat(3,auto);gap:6px 18px;text-align:right;align-items:start;}
|
||
.mi label{display:block;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.09em;color:var(--text-muted);margin-bottom:2px;}
|
||
.mi span.mv{font-family:'DM Mono',monospace;font-size:11px;font-weight:500;color:var(--accent2);}
|
||
.mi input[type="date"]{font-family:'DM Mono',monospace;font-size:11px;color:var(--accent2);border:none;background:transparent;padding:0;width:108px;}
|
||
.mi input[type="date"]:focus{outline:none;border-bottom:1px solid var(--accent2);box-shadow:none;}
|
||
|
||
/* ── LINES CARD ── */
|
||
.tcard{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);box-shadow:var(--shadow);overflow:visible;}
|
||
.tabs{display:flex;border-bottom:1px solid var(--border);background:var(--surface2);}
|
||
.tab{padding:8px 16px;font-size:11px;font-weight:600;color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;white-space:nowrap;transition:all .12s;user-select:none;}
|
||
.tab.active{color:var(--accent2);border-bottom-color:var(--accent2);}
|
||
.tab-pane{display:none;padding:12px 14px;}
|
||
.tab-pane.active{display:block;}
|
||
|
||
/* ── TABLE ── */
|
||
.lines-bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;}
|
||
.stitle{font-family:'Syne',sans-serif;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--text-label);}
|
||
.lines-wrap{overflow-x:auto;border:1px solid var(--border);border-radius:5px;}
|
||
table{width:100%;border-collapse:collapse;font-size:11px;}
|
||
thead tr{background:var(--surface2);}
|
||
th{padding:6px 6px;text-align:left;font-size:9px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.07em;white-space:nowrap;border-bottom:1px solid var(--border);position:sticky;top:0;z-index:5;background:var(--surface2);}
|
||
td{padding:0;border-bottom:1px solid var(--border);vertical-align:middle;position:relative;}
|
||
tbody tr:last-child td{border-bottom:none;}
|
||
tbody tr:hover td{background:#fdfaf6;}
|
||
td input,td select{background:transparent;border:none;border-radius:0;padding:5px 6px;font-size:11px;color:var(--text);width:100%;font-family:'DM Sans',sans-serif;height:28px;}
|
||
td input:focus,td select:focus{background:rgba(200,121,58,.05);box-shadow:inset 0 0 0 1.5px var(--accent2);outline:none;}
|
||
td select{appearance:none;background-image:none;cursor:pointer;}
|
||
td input[readonly]{background:var(--surface2);color:var(--text-muted);}
|
||
.nc{text-align:right;}
|
||
.nc input{text-align:right;font-family:'DM Mono',monospace;}
|
||
.li{display:block;width:26px;text-align:center;font-family:'DM Mono',monospace;font-size:10px;color:var(--text-muted);padding:5px 2px;}
|
||
.xbtn{display:block;width:24px;text-align:center;color:var(--text-muted);cursor:pointer;padding:4px 2px;font-size:12px;transition:color .12s;}
|
||
.xbtn:hover{color:var(--danger);}
|
||
|
||
/* ── TOTALS ── */
|
||
.totals-wrap{display:flex;justify-content:flex-end;padding:10px 12px;border-top:1px solid var(--border);background:var(--surface2);}
|
||
.tg{display:grid;grid-template-columns:180px 140px;gap:3px 8px;text-align:right;align-items:center;}
|
||
.tl{font-size:11px;color:var(--text-muted);}
|
||
.tv{font-family:'DM Mono',monospace;font-size:11px;color:var(--text);}
|
||
.tt-row .tl{font-weight:700;font-size:12px;color:var(--text);}
|
||
.tt-row .tv{font-size:14px;font-weight:600;color:var(--accent2);}
|
||
|
||
/* ── AUTOCOMPLETE ── */
|
||
.acw{position:relative;width:100%;}
|
||
.acd-fixed{position:fixed;min-width:240px;background:var(--surface);border:1.5px solid var(--accent2);border-radius:6px;box-shadow:0 8px 32px rgba(0,0,0,.18);z-index:99999;max-height:240px;overflow-y:auto;display:none;}
|
||
.acd-fixed.open{display:block;}
|
||
.aci{padding:7px 10px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;gap:8px;border-bottom:1px solid var(--border);transition:background .08s;}
|
||
.aci:last-child{border-bottom:none;}
|
||
.aci:hover,.aci.hi{background:#fef6ed;}
|
||
.ac-name{font-size:11px;color:var(--text);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||
.ac-code{font-family:'DM Mono',monospace;font-size:10px;color:var(--accent2);flex-shrink:0;}
|
||
.ac-msg{padding:8px 10px;color:var(--text-muted);font-size:11px;font-style:italic;}
|
||
.spin{display:inline-block;width:11px;height:11px;border:2px solid var(--border);border-top-color:var(--accent2);border-radius:50%;animation:sp .6s linear infinite;flex-shrink:0;}
|
||
@keyframes sp{to{transform:rotate(360deg)}}
|
||
|
||
/* ── OVERLAY & TOAST ── */
|
||
.overlay{display:none;position:fixed;inset:0;background:rgba(245,243,239,.85);backdrop-filter:blur(3px);z-index:9000;align-items:center;justify-content:center;flex-direction:column;gap:10px;}
|
||
.overlay.show{display:flex;}
|
||
.spinner{width:36px;height:36px;border:3px solid var(--border2);border-top-color:var(--accent2);border-radius:50%;animation:sp .7s linear infinite;}
|
||
.ta{position:fixed;top:60px;right:16px;z-index:9999;display:flex;flex-direction:column;gap:6px;pointer-events:none;}
|
||
.toast{padding:9px 13px;border-radius:6px;font-size:11px;font-weight:500;max-width:360px;box-shadow:var(--shadow-lg);display:flex;align-items:center;gap:7px;animation:tin .18s ease;pointer-events:all;}
|
||
@keyframes tin{from{opacity:0;transform:translateX(14px)}to{opacity:1}}
|
||
.t-ok{background:#edf7f1;color:var(--success);border:1px solid #a8d5bc;}
|
||
.t-err{background:#fdf0ee;color:var(--danger);border:1px solid #e8b0aa;}
|
||
.t-info{background:#fef6ed;color:var(--accent2);border:1px solid #e8c9a8;}
|
||
|
||
/* ── VIEW TAB TABLE ── */
|
||
.vtable-wrap{border:1px solid var(--border);border-radius:var(--r);overflow:hidden;margin-top:12px;}
|
||
.vtable-wrap table{font-size:12px;}
|
||
.vtable-wrap th{font-size:9px;padding:7px 10px;}
|
||
.vtable-wrap td{padding:6px 10px;border-bottom:1px solid var(--border);}
|
||
.vtable-wrap tbody tr:hover td{background:#fdfaf6;cursor:default;}
|
||
.doc-status{padding:2px 7px;border-radius:10px;font-size:9px;font-weight:700;font-family:'DM Mono',monospace;text-transform:uppercase;}
|
||
.ds-open{background:#edf7f1;color:var(--success);}
|
||
.ds-closed{background:var(--surface2);color:var(--text-muted);}
|
||
.ds-draft{background:#fff8ed;color:var(--accent2);}
|
||
.view-bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;}
|
||
.empty-state{text-align:center;padding:40px 20px;color:var(--text-muted);font-size:11px;}
|
||
|
||
hr.dv{border:none;border-top:1px solid var(--border);margin:10px 0;}
|
||
.resp{display:none;background:#1a1a1a;border-radius:6px;padding:12px;font-family:'DM Mono',monospace;font-size:10px;color:#a8d5bc;white-space:pre-wrap;word-break:break-all;max-height:280px;overflow-y:auto;margin-top:12px;line-height:1.7;}
|
||
</style>
|
||
<link rel="stylesheet" href="/theme.css?v=3"/>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- LOADING OVERLAY -->
|
||
<div class="overlay" id="overlay">
|
||
<div class="spinner"></div>
|
||
<span style="font-size:11px;color:var(--text-muted);font-family:'DM Mono',monospace;">Posting to SAP B1…</span>
|
||
</div>
|
||
|
||
<!-- TOAST AREA -->
|
||
<div class="ta" id="ta"></div>
|
||
|
||
<!-- FIXED AUTOCOMPLETE PORTAL -->
|
||
<div class="acd-fixed" id="acPortal"></div>
|
||
|
||
<!-- TOP BAR -->
|
||
<div class="topbar">
|
||
<div class="tb-brand">
|
||
<div class="tb-mark">PO</div>
|
||
<div>
|
||
<div class="tb-title">Purchase Order</div>
|
||
<div class="tb-sub">Purchase Orders · SAP B1</div>
|
||
</div>
|
||
</div>
|
||
<div class="tb-actions">
|
||
<select id="companySel" style="background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);color:#fff;border-radius:5px;padding:4px 22px 4px 8px;font-size:11px;font-family:'DM Mono',monospace;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23ffffff80'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right 6px center;appearance:none;cursor:pointer;" onchange="onCompanyChange()">
|
||
</select>
|
||
<span class="badge badge-draft" id="statusBadge">● DRAFT</span>
|
||
<button class="btn btn-ghost btn-sm" onclick="resetForm()">↺ Reset</button>
|
||
<button class="btn btn-primary btn-sm" id="postBtn" onclick="submitPO()">✓ Post PO</button>
|
||
<span class="user-chip" id="userChip"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PAGE TABS -->
|
||
<div class="page-tabs">
|
||
<div class="ptab active" onclick="switchPage(this,'pgCreate')">Create Order</div>
|
||
<div class="ptab" onclick="switchPage(this,'pgView')">View Orders</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════════ -->
|
||
<!-- CREATE TAB -->
|
||
<!-- ═══════════════════════════════════════════ -->
|
||
<div class="ptab-pane active" id="pgCreate">
|
||
<div class="main">
|
||
|
||
<!-- HEADER CARD -->
|
||
<div class="hcard">
|
||
<div class="hcard-row">
|
||
<!-- Left: Vendor + fields -->
|
||
<div>
|
||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:8px;margin-bottom:8px;">
|
||
<!-- Vendor Autocomplete -->
|
||
<div class="field">
|
||
<label>Vendor <span class="req">*</span></label>
|
||
<div class="acw">
|
||
<input type="text" id="vendorSearch" placeholder="Search vendor code or name…" autocomplete="off" oninput="vendorAc(this.value)" onfocus="if(this.value.length>1)vendorAc(this.value)">
|
||
</div>
|
||
<input type="hidden" id="cardCode">
|
||
<input type="hidden" id="cardName">
|
||
</div>
|
||
<div class="field">
|
||
<label>Vendor Ref. No.</label>
|
||
<input type="text" id="numAtCard" placeholder="e.g. INV-2024-001">
|
||
</div>
|
||
<div class="field">
|
||
<label>Delivery Date <span class="req">*</span></label>
|
||
<input type="date" id="docDueDate">
|
||
</div>
|
||
</div>
|
||
<hr class="dv" style="margin:8px 0;">
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;max-width:480px;">
|
||
<div class="field">
|
||
<label>Remarks</label>
|
||
<textarea id="comments" rows="2" placeholder="Order remarks / notes…"></textarea>
|
||
</div>
|
||
<div class="field">
|
||
<label>Branch</label>
|
||
<select id="bplId" onchange="onBranchChange()"><option value="">— Select —</option></select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Right: Meta strip -->
|
||
<div class="meta-strip">
|
||
<div class="mi">
|
||
<label>Doc No.</label>
|
||
<span class="mv" id="docNumDisp">AUTO</span>
|
||
</div>
|
||
<div class="mi">
|
||
<label>Posting Date</label>
|
||
<input type="date" id="docDate">
|
||
</div>
|
||
<div class="mi">
|
||
<label>Document Date</label>
|
||
<input type="date" id="taxDate">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- LINES CARD -->
|
||
<div class="tcard">
|
||
<div class="tabs">
|
||
<div class="tab active" onclick="switchTab(this,'tcContents')">Contents</div>
|
||
</div>
|
||
|
||
<div class="tab-pane active" id="tcContents">
|
||
<div class="lines-bar">
|
||
<span class="stitle">Document Lines</span>
|
||
<div style="display:flex;gap:6px;">
|
||
<button class="btn btn-outline btn-sm" onclick="addLine()">+ Add Line</button>
|
||
<button class="btn btn-outline btn-sm" onclick="clearLines()">Clear</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="lines-wrap">
|
||
<table id="linesTable">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:28px;">#</th>
|
||
<th style="min-width:110px;">Item Code <span style="color:var(--accent2);">*</span></th>
|
||
<th style="min-width:160px;">Description</th>
|
||
<th style="min-width:60px;" class="nc">Qty <span style="color:var(--accent2);">*</span></th>
|
||
<th style="min-width:55px;">Unit</th>
|
||
<th style="min-width:100px;" class="nc">Unit Price <span style="color:var(--accent2);">*</span></th>
|
||
<th style="min-width:60px;" class="nc">Disc %</th>
|
||
<th style="min-width:120px;">Tax Code</th>
|
||
<th style="min-width:50px;" class="nc">Tax %</th>
|
||
<th style="min-width:100px;" class="nc">Line Total</th>
|
||
<th style="min-width:80px;">Warehouse</th>
|
||
<th style="width:26px;"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- TOTALS -->
|
||
<div class="totals-wrap">
|
||
<div class="tg">
|
||
<span class="tl">Before Discount</span>
|
||
<span class="tv" id="totPre">0.00</span>
|
||
<span class="tl">Discount</span>
|
||
<span class="tv" id="totDisc">0.00</span>
|
||
<span class="tl">Tax</span>
|
||
<span class="tv" id="totTax">0.00</span>
|
||
<span class="tl tt-row">Total Payment Due</span>
|
||
<span class="tv tt-row" id="totDoc">0.00</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<pre class="resp" id="respPanel"></pre>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════════ -->
|
||
<!-- VIEW ORDERS TAB -->
|
||
<!-- ═══════════════════════════════════════════ -->
|
||
<div class="ptab-pane" id="pgView">
|
||
<div class="main">
|
||
<div class="hcard">
|
||
<div class="view-bar">
|
||
<div>
|
||
<span class="stitle">Purchase Orders</span>
|
||
<span style="font-size:10px;color:var(--text-muted);margin-left:8px;font-family:'DM Mono',monospace;" id="viewCount"></span>
|
||
</div>
|
||
<button class="btn btn-outline btn-sm" onclick="loadOrders()">↻ Refresh</button>
|
||
</div>
|
||
<div id="viewTableWrap">
|
||
<div class="empty-state">Click <strong>Refresh</strong> or switch to this tab to load orders.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// ═══════════════════════════════════════════
|
||
// AUTH & CONFIG
|
||
// ═══════════════════════════════════════════
|
||
const API = '';
|
||
const PO_ENDPOINT = '/api/sap/purchase-order';
|
||
|
||
let TOKEN = sessionStorage.getItem('portal_token') || '';
|
||
if (!TOKEN) { window.location.href = '/'; }
|
||
|
||
let lineSeq = 0;
|
||
const _timers = {};
|
||
let _taxCodes = [];
|
||
let _warehouses = [];
|
||
let _branches = [];
|
||
let _portalTarget = null;
|
||
let _ordersLoaded = false;
|
||
|
||
const ah = () => TOKEN ? { 'Authorization': `Bearer ${TOKEN}` } : {};
|
||
const esc = s => String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||
const fmt = n => parseFloat(n || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
const company = () => document.getElementById('companySel').value;
|
||
|
||
// ═══════════════════════════════════════════
|
||
// TOAST
|
||
// ═══════════════════════════════════════════
|
||
function toast(msg, type = 'info') {
|
||
const el = document.createElement('div');
|
||
el.className = `toast t-${type}`;
|
||
const icon = { ok: '✓', err: '✕', info: 'ℹ' }[type] || 'ℹ';
|
||
el.innerHTML = `<span>${icon}</span><span>${esc(String(msg))}</span>`;
|
||
document.getElementById('ta').appendChild(el);
|
||
setTimeout(() => el.remove(), 5000);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// PAGE / INNER TAB SWITCHING
|
||
// ═══════════════════════════════════════════
|
||
function switchPage(el, pid) {
|
||
document.querySelectorAll('.ptab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.ptab-pane').forEach(p => p.classList.remove('active'));
|
||
el.classList.add('active');
|
||
document.getElementById(pid).classList.add('active');
|
||
if (pid === 'pgView' && !_ordersLoaded) loadOrders();
|
||
}
|
||
|
||
function switchTab(el, pid) {
|
||
el.closest('.tcard').querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
el.closest('.tcard').querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
|
||
el.classList.add('active');
|
||
document.getElementById(pid).classList.add('active');
|
||
}
|
||
|
||
function onCompanyChange() {
|
||
loadBranches();
|
||
loadWarehouses();
|
||
loadTaxCodes();
|
||
if (_ordersLoaded) { _ordersLoaded = false; }
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// INIT
|
||
// ═══════════════════════════════════════════
|
||
async function populateCompanySelect(){
|
||
try{
|
||
const list=await window.fetchCompanyList();
|
||
if(list.length){
|
||
const sel=document.getElementById('companySel');
|
||
const cur=sel.value;
|
||
sel.innerHTML=list.map(c=>`<option value="${esc(c.value)}">${esc(c.label)}</option>`).join('');
|
||
if(list.some(c=>c.value===cur))sel.value=cur;
|
||
}
|
||
}catch(e){}
|
||
}
|
||
|
||
async function initPage() {
|
||
await populateCompanySelect();
|
||
const today = new Date().toISOString().slice(0, 10);
|
||
document.getElementById('docDate').value = today;
|
||
document.getElementById('taxDate').value = today;
|
||
document.getElementById('docDueDate').value = today;
|
||
|
||
// Try to get username
|
||
try {
|
||
const r = await fetch(`${API}/api/auth/me`, { headers: ah() });
|
||
const d = await r.json();
|
||
if (d.success) document.getElementById('userChip').textContent = d.user?.name || d.user?.username || '';
|
||
} catch(e) {}
|
||
|
||
await Promise.all([loadBranches(), loadWarehouses(), loadTaxCodes()]);
|
||
addLine();
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// LOOKUPS
|
||
// ═══════════════════════════════════════════
|
||
async function loadTaxCodes() {
|
||
const fallback = [
|
||
{ Code: 'CG+SG@5', Rate: 5 },
|
||
{ Code: 'CG+SG@12', Rate: 12 },
|
||
{ Code: 'CG+SG@18', Rate: 18 },
|
||
{ Code: 'CG+SG@28', Rate: 28 },
|
||
{ Code: 'IGST@5', Rate: 5 },
|
||
{ Code: 'IGST@18', Rate: 18 },
|
||
{ Code: 'EXEMPT', Rate: 0 },
|
||
];
|
||
try {
|
||
const res = await fetch(`${API}/api/sap/lookup/tax-codes?company=${company()}`, { headers: ah() });
|
||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||
const d = await res.json();
|
||
_taxCodes = (d.data && d.data.length) ? d.data : fallback;
|
||
} catch(e) {
|
||
console.warn('[PO] tax-codes fallback:', e.message);
|
||
_taxCodes = fallback;
|
||
}
|
||
document.querySelectorAll('.taxcode-sel').forEach(s => fillTaxSel(s));
|
||
}
|
||
|
||
async function loadWarehouses() {
|
||
try {
|
||
const res = await fetch(`${API}/api/sap/lookup/warehouses?company=${company()}`, { headers: ah() });
|
||
const d = await res.json();
|
||
_warehouses = d.data || [];
|
||
document.querySelectorAll('.whs-sel').forEach(s => fillWhsSel(s));
|
||
} catch(e) { console.warn('[PO] warehouses:', e.message); }
|
||
}
|
||
|
||
async function loadBranches() {
|
||
try {
|
||
const res = await fetch(`${API}/api/sap/lookup/branches?company=${company()}`, { headers: ah() });
|
||
const d = await res.json();
|
||
_branches = d.data || [];
|
||
const sel = document.getElementById('bplId');
|
||
sel.innerHTML = '<option value="">— Select Branch —</option>' +
|
||
_branches.map(b => `<option value="${esc(b.BPLId)}">${esc(b.BPLName)}</option>`).join('');
|
||
} catch(e) { console.warn('[PO] branches:', e.message); }
|
||
}
|
||
|
||
function onBranchChange() {}
|
||
|
||
function fillTaxSel(sel, defaultCode) {
|
||
const prev = defaultCode || sel.value;
|
||
sel.innerHTML = '<option value="">— Tax —</option>' +
|
||
_taxCodes.map(t =>
|
||
`<option value="${esc(t.Code)}" data-rate="${t.Rate || 0}">${esc(t.Code)}</option>`
|
||
).join('');
|
||
if (prev) {
|
||
const opt = sel.querySelector(`option[value="${CSS.escape(prev)}"]`);
|
||
if (opt) sel.value = prev;
|
||
}
|
||
sel.onchange = () => {
|
||
const opt = sel.options[sel.selectedIndex];
|
||
const rate = parseFloat(opt?.dataset?.rate || 0);
|
||
const row = sel.closest('tr');
|
||
if (row) {
|
||
const tp = row.querySelector('.taxpct-inp');
|
||
if (tp) { tp.value = rate; recalc(); }
|
||
}
|
||
};
|
||
}
|
||
|
||
function fillWhsSel(sel, defaultCode) {
|
||
const prev = defaultCode || sel.value;
|
||
sel.innerHTML = '<option value="">— WH —</option>' +
|
||
_warehouses.map(w =>
|
||
`<option value="${esc(w.WarehouseCode)}">${esc(w.WarehouseCode)} – ${esc(w.WarehouseName)}</option>`
|
||
).join('');
|
||
if (prev) {
|
||
const opt = sel.querySelector(`option[value="${CSS.escape(prev)}"]`);
|
||
if (opt) sel.value = prev;
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// AUTOCOMPLETE ENGINE
|
||
// ═══════════════════════════════════════════
|
||
const portal = document.getElementById('acPortal');
|
||
|
||
function positionPortal(inputEl) {
|
||
const r = inputEl.getBoundingClientRect();
|
||
portal.style.left = r.left + 'px';
|
||
portal.style.top = (r.bottom + 2) + 'px';
|
||
portal.style.width = Math.max(260, r.width) + 'px';
|
||
}
|
||
|
||
function openPortal(inputEl, items, onSelect) {
|
||
_portalTarget = { inputEl, onSelect };
|
||
portal.innerHTML = '';
|
||
if (!items.length) {
|
||
portal.innerHTML = '<div class="ac-msg">No results found</div>';
|
||
} else {
|
||
items.forEach(item => {
|
||
const div = document.createElement('div');
|
||
div.className = 'aci';
|
||
div.innerHTML = `<span class="ac-name">${esc(item.label)}</span><span class="ac-code">${esc(item.code)}</span>`;
|
||
div.addEventListener('mousedown', e => { e.preventDefault(); onSelect(item); closePortal(); });
|
||
portal.appendChild(div);
|
||
});
|
||
}
|
||
positionPortal(inputEl);
|
||
portal.classList.add('open');
|
||
}
|
||
|
||
function openPortalLoading(inputEl) {
|
||
portal.innerHTML = '<div class="aci"><div class="spin"></div><span class="ac-name" style="margin-left:6px;">Searching…</span></div>';
|
||
positionPortal(inputEl);
|
||
portal.classList.add('open');
|
||
}
|
||
|
||
function openPortalMsg(inputEl, msg) {
|
||
portal.innerHTML = `<div class="ac-msg">${esc(msg)}</div>`;
|
||
positionPortal(inputEl);
|
||
portal.classList.add('open');
|
||
}
|
||
|
||
function closePortal() {
|
||
portal.classList.remove('open');
|
||
portal.innerHTML = '';
|
||
_portalTarget = null;
|
||
}
|
||
|
||
document.addEventListener('mousedown', e => {
|
||
if (!portal.contains(e.target) && !e.target.closest('.acw')) closePortal();
|
||
});
|
||
window.addEventListener('scroll', () => {
|
||
if (_portalTarget && portal.classList.contains('open')) positionPortal(_portalTarget.inputEl);
|
||
}, true);
|
||
|
||
// ── Vendor Autocomplete ───────────────────
|
||
function vendorAc(q) {
|
||
clearTimeout(_timers.vendor);
|
||
if (!q || q.length < 2) { closePortal(); return; }
|
||
openPortalLoading(document.getElementById('vendorSearch'));
|
||
_timers.vendor = setTimeout(async () => {
|
||
try {
|
||
const res = await fetch(`${API}/api/sap/lookup/vendors?q=${encodeURIComponent(q)}&company=${company()}`, { headers: ah() });
|
||
const data = await res.json();
|
||
const list = (data.data || []).map(v => ({ label: v.CardName, code: v.CardCode, raw: v }));
|
||
if (!list.length) { openPortalMsg(document.getElementById('vendorSearch'), 'No vendors found'); return; }
|
||
openPortal(document.getElementById('vendorSearch'), list, item => {
|
||
document.getElementById('cardCode').value = item.raw.CardCode;
|
||
document.getElementById('cardName').value = item.raw.CardName;
|
||
document.getElementById('vendorSearch').value = `${item.raw.CardCode} — ${item.raw.CardName}`;
|
||
toast(`Vendor: ${item.raw.CardName}`, 'ok');
|
||
});
|
||
} catch(err) { openPortalMsg(document.getElementById('vendorSearch'), 'Error: ' + err.message); }
|
||
}, 300);
|
||
}
|
||
|
||
// ── Item Autocomplete ─────────────────────
|
||
function itemAcForInput(inputEl, onSelect) {
|
||
const q = inputEl.value;
|
||
clearTimeout(_timers['item_' + inputEl.id]);
|
||
if (!q || q.length < 2) { closePortal(); return; }
|
||
openPortalLoading(inputEl);
|
||
_timers['item_' + inputEl.id] = setTimeout(async () => {
|
||
try {
|
||
const res = await fetch(`${API}/api/sap/lookup/items?q=${encodeURIComponent(q)}&company=${company()}`, { headers: ah() });
|
||
const data = await res.json();
|
||
const list = (data.data || []).map(i => ({ label: i.ItemName, code: i.ItemCode, raw: i }));
|
||
if (!list.length) { openPortalMsg(inputEl, 'No items found'); return; }
|
||
openPortal(inputEl, list, item => onSelect(item.raw));
|
||
} catch(err) { openPortalMsg(inputEl, 'Error: ' + err.message); }
|
||
}, 300);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// LINE MANAGEMENT
|
||
// ═══════════════════════════════════════════
|
||
function addLine(d = {}) {
|
||
lineSeq++;
|
||
const lid = lineSeq;
|
||
const tr = document.createElement('tr');
|
||
tr.id = `line-${lid}`;
|
||
|
||
const rowNum = document.querySelectorAll('#tbody tr').length + 1;
|
||
|
||
tr.innerHTML = `
|
||
<td><span class="li">${rowNum}</span></td>
|
||
<td style="min-width:110px;">
|
||
<input class="icode-inp" type="text" id="icode-${lid}" placeholder="Item Code"
|
||
value="${esc(d.itemCode || '')}"
|
||
oninput="itemAcForInput(this, function(i){selItem(${lid},i)})"
|
||
onfocus="if(this.value.length>=2)itemAcForInput(this,function(i){selItem(${lid},i)})">
|
||
</td>
|
||
<td style="min-width:160px;"><input class="desc-inp" type="text" id="desc-${lid}" placeholder="Description" value="${esc(d.desc || '')}"></td>
|
||
<td class="nc" style="min-width:60px;"><input class="qty-inp" type="number" id="qty-${lid}" value="${d.qty || 1}" min="0" step="0.001" oninput="recalc()"></td>
|
||
<td style="min-width:55px;"><input class="uom-inp" type="text" id="uom-${lid}" value="${esc(d.uom || '')}" placeholder="UOM" readonly style="text-align:center;font-size:10px;color:var(--text-muted);background:var(--surface2);"></td>
|
||
<td class="nc" style="min-width:100px;"><input class="price-inp" type="number" id="price-${lid}" value="${d.price || 0}" min="0" step="0.01" oninput="recalc()"></td>
|
||
<td class="nc" style="min-width:60px;"><input class="disc-inp" type="number" id="disc-${lid}" value="${d.disc || 0}" min="0" max="100" step="0.01" oninput="recalc()"></td>
|
||
<td style="min-width:120px;"><select class="taxcode-sel" id="tax-${lid}"></select></td>
|
||
<td class="nc" style="min-width:50px;"><input class="taxpct-inp" type="number" id="tp-${lid}" value="${d.taxPct || 0}" min="0" max="100" oninput="recalc()" style="color:var(--text-muted);font-size:10px;"></td>
|
||
<td class="nc" style="min-width:100px;"><input class="linetot-inp" type="text" id="lt-${lid}" readonly value="0.00" style="color:var(--accent2);font-weight:600;font-family:'DM Mono',monospace;"></td>
|
||
<td style="min-width:80px;"><select class="whs-sel" id="whs-${lid}"></select></td>
|
||
<td><span class="xbtn" onclick="removeLine(${lid})">✕</span></td>
|
||
`;
|
||
|
||
document.getElementById('tbody').appendChild(tr);
|
||
|
||
const taxSel = tr.querySelector('.taxcode-sel');
|
||
if (taxSel) fillTaxSel(taxSel, d.taxCode);
|
||
|
||
const whsSel = tr.querySelector('.whs-sel');
|
||
if (whsSel) fillWhsSel(whsSel, d.whs);
|
||
|
||
renumber();
|
||
recalc();
|
||
}
|
||
|
||
function selItem(lid, item) {
|
||
const icode = document.getElementById(`icode-${lid}`);
|
||
const desc = document.getElementById(`desc-${lid}`);
|
||
const uom = document.getElementById(`uom-${lid}`);
|
||
if (icode) icode.value = item.ItemCode;
|
||
if (desc) desc.value = item.ItemName || '';
|
||
if (uom) uom.value = item.InventoryUOM || '';
|
||
recalc();
|
||
toast(`Item: ${item.ItemCode} — ${item.ItemName}`, 'info');
|
||
}
|
||
|
||
function removeLine(lid) {
|
||
document.getElementById(`line-${lid}`)?.remove();
|
||
renumber();
|
||
recalc();
|
||
}
|
||
|
||
function clearLines() {
|
||
if (!confirm('Remove all lines?')) return;
|
||
document.getElementById('tbody').innerHTML = '';
|
||
lineSeq = 0;
|
||
recalc();
|
||
}
|
||
|
||
function renumber() {
|
||
document.querySelectorAll('#tbody tr').forEach((tr, i) => {
|
||
const s = tr.querySelector('.li');
|
||
if (s) s.textContent = i + 1;
|
||
});
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// RECALCULATE TOTALS
|
||
// ═══════════════════════════════════════════
|
||
function recalc() {
|
||
let sumPre = 0, sumDisc = 0, sumTax = 0;
|
||
|
||
document.querySelectorAll('#tbody tr').forEach(tr => {
|
||
const qty = parseFloat(tr.querySelector('.qty-inp')?.value || 0);
|
||
const price = parseFloat(tr.querySelector('.price-inp')?.value || 0);
|
||
const disc = parseFloat(tr.querySelector('.disc-inp')?.value || 0);
|
||
const taxPct = parseFloat(tr.querySelector('.taxpct-inp')?.value|| 0);
|
||
|
||
const gross = qty * price;
|
||
const discAmt = gross * disc / 100;
|
||
const lineNet = gross - discAmt;
|
||
const lineTax = lineNet * taxPct / 100;
|
||
const lineTotal= lineNet + lineTax;
|
||
|
||
const ltInp = tr.querySelector('.linetot-inp');
|
||
if (ltInp) ltInp.value = lineTotal.toFixed(2);
|
||
|
||
sumPre += gross;
|
||
sumDisc += discAmt;
|
||
sumTax += lineTax;
|
||
});
|
||
|
||
const total = sumPre - sumDisc + sumTax;
|
||
document.getElementById('totPre').textContent = fmt(sumPre);
|
||
document.getElementById('totDisc').textContent = fmt(sumDisc);
|
||
document.getElementById('totTax').textContent = fmt(sumTax);
|
||
document.getElementById('totDoc').textContent = fmt(total);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// BUILD PAYLOAD
|
||
// ═══════════════════════════════════════════
|
||
function buildPayload() {
|
||
const cardCode = document.getElementById('cardCode').value.trim();
|
||
const docDate = document.getElementById('docDate').value;
|
||
const dueDate = document.getElementById('docDueDate').value;
|
||
const numAtCard = document.getElementById('numAtCard').value.trim();
|
||
const comments = document.getElementById('comments').value.trim();
|
||
const bplId = parseInt(document.getElementById('bplId').value) || undefined;
|
||
|
||
const lines = [];
|
||
document.querySelectorAll('#tbody tr').forEach((tr, idx) => {
|
||
const itemCode = tr.querySelector('.icode-inp')?.value?.trim() || '';
|
||
const desc = tr.querySelector('.desc-inp')?.value || '';
|
||
const qty = parseFloat(tr.querySelector('.qty-inp')?.value || 0);
|
||
const price = parseFloat(tr.querySelector('.price-inp')?.value|| 0);
|
||
const disc = parseFloat(tr.querySelector('.disc-inp')?.value || 0);
|
||
const taxCode = tr.querySelector('.taxcode-sel')?.value || '';
|
||
const whs = tr.querySelector('.whs-sel')?.value || '';
|
||
if (!itemCode && !desc && qty === 0 && price === 0) return; // skip empty
|
||
const line = {
|
||
ItemCode: itemCode,
|
||
ItemDescription: desc,
|
||
Quantity: qty,
|
||
UnitPrice: price,
|
||
DiscountPercent: disc,
|
||
TaxCode: taxCode || undefined,
|
||
};
|
||
if (whs) line.WarehouseCode = whs;
|
||
lines.push(line);
|
||
});
|
||
|
||
const payload = {
|
||
company: company(),
|
||
CardCode: cardCode,
|
||
DocDate: docDate,
|
||
DocDueDate: dueDate,
|
||
Comments: comments || undefined,
|
||
DocumentLines: lines,
|
||
};
|
||
|
||
if (numAtCard) payload.NumAtCard = numAtCard;
|
||
if (bplId) payload.BPL_IDAssignedToInvoice = bplId;
|
||
|
||
// Clean undefined
|
||
Object.keys(payload).forEach(k => { if (payload[k] === undefined) delete payload[k]; });
|
||
|
||
return payload;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// SUBMIT
|
||
// ═══════════════════════════════════════════
|
||
async function submitPO() {
|
||
const cardCode = document.getElementById('cardCode').value.trim();
|
||
if (!cardCode) return toast('Select a vendor first.', 'err');
|
||
if (!document.getElementById('docDate').value) return toast('Posting Date is required.', 'err');
|
||
if (!document.getElementById('docDueDate').value) return toast('Delivery Date is required.', 'err');
|
||
|
||
const rows = document.querySelectorAll('#tbody tr');
|
||
if (!rows.length) return toast('Add at least one line item.', 'err');
|
||
|
||
let hasItem = false;
|
||
let valid = true;
|
||
rows.forEach(tr => {
|
||
const ic = tr.querySelector('.icode-inp')?.value?.trim();
|
||
const qty = parseFloat(tr.querySelector('.qty-inp')?.value || 0);
|
||
const pr = parseFloat(tr.querySelector('.price-inp')?.value || 0);
|
||
if (ic) hasItem = true;
|
||
if (ic && (qty <= 0 || pr < 0)) valid = false;
|
||
});
|
||
if (!hasItem) return toast('At least one line must have an Item Code.', 'err');
|
||
if (!valid) return toast('Check line Qty and Unit Price values.', 'err');
|
||
|
||
const payload = buildPayload();
|
||
document.getElementById('overlay').classList.add('show');
|
||
document.getElementById('postBtn').disabled = true;
|
||
|
||
try {
|
||
const res = await fetch(`${API}${PO_ENDPOINT}`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', ...ah() },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
const data = await res.json();
|
||
document.getElementById('overlay').classList.remove('show');
|
||
document.getElementById('postBtn').disabled = false;
|
||
|
||
if (res.ok && (data?.DocEntry || data?.DocNum)) {
|
||
const docNum = data.DocNum || data.DocEntry;
|
||
document.getElementById('statusBadge').textContent = '● OPEN';
|
||
document.getElementById('statusBadge').className = 'badge badge-open';
|
||
document.getElementById('docNumDisp').textContent = docNum;
|
||
toast(`Purchase Order #${docNum} created successfully!`, 'ok');
|
||
_ordersLoaded = false;
|
||
setTimeout(() => resetForm(true), 800);
|
||
} else {
|
||
const sapMsg = data?.error?.message?.value || data?.message || JSON.stringify(data).slice(0, 200);
|
||
toast(`SAP Error: ${sapMsg}`, 'err');
|
||
document.getElementById('respPanel').textContent = JSON.stringify(data, null, 2);
|
||
document.getElementById('respPanel').style.display = 'block';
|
||
}
|
||
} catch(err) {
|
||
document.getElementById('overlay').classList.remove('show');
|
||
document.getElementById('postBtn').disabled = false;
|
||
toast('Network error: ' + err.message, 'err');
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// RESET
|
||
// ═══════════════════════════════════════════
|
||
function resetForm(silent = false) {
|
||
if (!silent && !confirm('Reset the form? All unsaved data will be lost.')) return;
|
||
['vendorSearch','cardCode','cardName','numAtCard','comments'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.value = '';
|
||
});
|
||
const today = new Date().toISOString().slice(0, 10);
|
||
['docDate','taxDate','docDueDate'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.value = today;
|
||
});
|
||
document.getElementById('bplId').value = '';
|
||
document.getElementById('tbody').innerHTML = '';
|
||
lineSeq = 0;
|
||
document.getElementById('statusBadge').textContent = '● DRAFT';
|
||
document.getElementById('statusBadge').className = 'badge badge-draft';
|
||
document.getElementById('docNumDisp').textContent = 'AUTO';
|
||
document.getElementById('respPanel').style.display = 'none';
|
||
closePortal();
|
||
addLine();
|
||
if (!silent) toast('Form reset.', 'info');
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// VIEW ORDERS
|
||
// ═══════════════════════════════════════════
|
||
async function loadOrders() {
|
||
const wrap = document.getElementById('viewTableWrap');
|
||
wrap.innerHTML = '<div class="empty-state"><div class="spin" style="width:20px;height:20px;margin:0 auto 8px;"></div>Loading orders…</div>';
|
||
|
||
try {
|
||
const url = `${API}/api/sap/documents/PurchaseOrders?company=${encodeURIComponent(company())}&$top=50&$orderby=DocEntry+desc`;
|
||
const res = await fetch(url, { headers: ah() });
|
||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||
const data = await res.json();
|
||
|
||
const orders = data.value || data.data || data || [];
|
||
_ordersLoaded = true;
|
||
document.getElementById('viewCount').textContent = `${orders.length} record(s)`;
|
||
|
||
if (!orders.length) {
|
||
wrap.innerHTML = '<div class="empty-state">No purchase orders found for this company.</div>';
|
||
return;
|
||
}
|
||
|
||
const rows = orders.map(o => {
|
||
const st = o.DocumentStatus || '';
|
||
const stCls = st === 'bost_Open' ? 'ds-open' : st === 'bost_Close' ? 'ds-closed' : 'ds-draft';
|
||
const stLbl = st === 'bost_Open' ? 'Open' : st === 'bost_Close' ? 'Closed' : 'Draft';
|
||
const dueD = o.DocDueDate ? o.DocDueDate.slice(0, 10) : '—';
|
||
const docD = o.DocDate ? o.DocDate.slice(0, 10) : '—';
|
||
const total = parseFloat(o.DocTotal || 0).toLocaleString('en-IN', { minimumFractionDigits: 2 });
|
||
return `<tr>
|
||
<td style="font-family:'DM Mono',monospace;font-size:11px;font-weight:600;color:var(--accent2);">${esc(o.DocNum || '')}</td>
|
||
<td style="font-family:'DM Mono',monospace;font-size:11px;">${esc(docD)}</td>
|
||
<td><span style="font-size:11px;">${esc(o.CardCode || '')} <span style="color:var(--text-muted);">— ${esc(o.CardName || '')}</span></span></td>
|
||
<td style="font-family:'DM Mono',monospace;font-size:11px;">${esc(dueD)}</td>
|
||
<td style="font-family:'DM Mono',monospace;font-size:12px;font-weight:600;text-align:right;">₹ ${total}</td>
|
||
<td><span class="doc-status ${stCls}">${stLbl}</span></td>
|
||
</tr>`;
|
||
}).join('');
|
||
|
||
wrap.innerHTML = `<div class="vtable-wrap"><table>
|
||
<thead><tr>
|
||
<th>Doc #</th>
|
||
<th>Doc Date</th>
|
||
<th>Vendor</th>
|
||
<th>Due Date</th>
|
||
<th style="text-align:right;">Doc Total</th>
|
||
<th>Status</th>
|
||
</tr></thead>
|
||
<tbody>${rows}</tbody>
|
||
</table></div>`;
|
||
} catch(err) {
|
||
wrap.innerHTML = `<div class="empty-state" style="color:var(--danger);">Failed to load orders: ${esc(err.message)}</div>`;
|
||
toast('Error loading orders: ' + err.message, 'err');
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════
|
||
// BOOT
|
||
// ═══════════════════════════════════════════
|
||
initPage();
|
||
</script>
|
||
<script src="/sidebar.js?v=25"></script>
|
||
</body>
|
||
</html>
|