Files
John 69b4e68baf
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s
first commit
2026-09-23 17:31:02 +05:30

1213 lines
63 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/auth-guard.js?v=4"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GRPO — Goods Receipt PO</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;}
.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:15px;color:#fff;}
.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;}
.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-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;}
.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;}
.main{max-width:100%;padding:14px 18px 80px;}
.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;}
.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:60px;}
.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;}
.g5{display:grid;grid-template-columns:1fr 1fr 1fr 1fr 1fr;gap:8px;}
.s2{grid-column:span 2;}.s3{grid-column:span 3;}.s4{grid-column:span 4;}
.meta-strip{display:grid;grid-template-columns:repeat(5,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{font-family:'DM Mono',monospace;font-size:11px;font-weight:500;color:var(--accent2);}
.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;}
.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;position:relative;}
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-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:170px 130px;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);}
.tv input{background:transparent;border:none;border-bottom:1px solid var(--border);padding:1px 2px;font-family:'DM Mono',monospace;font-size:11px;color:var(--text);text-align:right;width:100%;}
.tv input:focus{outline:none;border-bottom-color:var(--accent2);}
.tt-row .tl{font-weight:700;font-size:12px;color:var(--text);}
.tt-row .tv{font-size:14px;font-weight:600;color:var(--accent2);}
.acw{position:relative;width:100%;}
.acd-fixed{position:fixed;min-width:220px;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{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:340px;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;}
.mbg{display:none;position:fixed;inset:0;background:rgba(26,26,26,.6);backdrop-filter:blur(5px);z-index:10000;align-items:center;justify-content:center;}
.mbg.show{display:flex;}
.modal{background:var(--surface);border-radius:10px;padding:26px 28px;width:340px;box-shadow:var(--shadow-lg);}
.modal h2{font-family:'Syne',sans-serif;font-size:18px;font-weight:700;margin-bottom:4px;}
.modal p{color:var(--text-muted);font-size:11px;margin-bottom:16px;}
.m-err{background:#fdf0ee;color:var(--danger);border:1px solid #e8b0aa;border-radius:5px;padding:7px 10px;font-size:11px;margin-bottom:10px;display:none;}
.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;}
hr.dv{border:none;border-top:1px solid var(--border);margin:10px 0;}
/* ── Attachments ── */
.attach-zone{border:2px dashed var(--border2);border-radius:var(--r);padding:20px 14px;background:var(--surface2);text-align:center;cursor:pointer;transition:all .2s;position:relative;min-height:100px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:4px;}
.attach-zone:hover,.attach-zone.drag{border-color:var(--accent2);background:rgba(200,121,58,.05);}
.attach-zone input[type=file]{position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%;z-index:2;}
.az-icon{font-size:22px;line-height:1;}
.az-label{font-size:11px;font-weight:600;color:var(--text-label);}
.az-sub{font-size:9px;color:var(--text-muted);}
.attach-preview{display:flex;align-items:center;gap:8px;padding:7px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:5px;margin-top:5px;}
.ap-thumb{width:32px;height:32px;border-radius:5px;font-size:16px;display:flex;align-items:center;justify-content:center;flex-shrink:0;overflow:hidden;}
.ap-thumb img{width:100%;height:100%;object-fit:cover;border-radius:4px;}
.ap-info{flex:1;min-width:0;}
.ap-name{font-size:11px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.ap-size{font-size:9px;color:var(--text-muted);margin-top:1px;}
.ap-rm{padding:2px 8px;font-size:9px;border:1.5px solid var(--danger);color:var(--danger);background:transparent;cursor:pointer;border-radius:3px;font-weight:600;flex-shrink:0;}
.att-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;padding:10px 0;}
.att-label{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.09em;color:var(--text-muted);margin-bottom:4px;}
</style>
<link rel="stylesheet" href="/theme.css?v=3"/>
</head>
<body>
<!-- LOGIN -->
<div class="mbg show" id="loginModal">
<div class="modal">
<h2>Sign In</h2>
<p>Portal credentials required to access GRPO.</p>
<div class="m-err" id="loginErr"></div>
<div class="field" style="margin-bottom:9px;"><label>Username</label><input id="lu" type="text" autocomplete="username" placeholder="username"></div>
<div class="field" style="margin-bottom:13px;"><label>Password</label><input id="lp" type="password" autocomplete="current-password" placeholder="••••••••"></div>
<button class="btn btn-primary" style="width:100%;" onclick="doLogin()">Sign In →</button>
</div>
</div>
<!-- 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…</span></div>
<div class="ta" id="ta"></div>
<!-- TOPBAR -->
<div class="topbar">
<div class="tb-brand">
<div class="tb-mark">G</div>
<div><div class="tb-title">Goods Receipt PO</div><div class="tb-sub">Purchase Delivery Notes · SAP B1</div></div>
</div>
<div class="tb-actions">
<span class="badge badge-draft" id="statusBadge">● DRAFT</span>
<button class="btn btn-ghost btn-sm" onclick="toggleResp()">{} Payload</button>
<button class="btn btn-ghost btn-sm" onclick="resetForm()">↺ Reset</button>
<button class="btn btn-primary btn-sm" onclick="submitGRPO()">✓ Post GRPO</button>
<span id="userChip" style="font-family:'DM Mono',monospace;font-size:9px;color:rgba(255,255,255,.4);margin-left:2px;"></span>
</div>
</div>
<!-- MAIN -->
<div class="main">
<!-- HEADER CARD -->
<div class="hcard">
<div style="display:grid;grid-template-columns:1fr auto;gap:16px;align-items:start;margin-bottom:10px;">
<div class="g4">
<div class="field s2">
<label>Vendor <span class="req">*</span></label>
<div class="acw" id="vendorWrap">
<input type="text" id="vendorSearch" placeholder="Search vendor code or name…" autocomplete="off" oninput="vendorAc(this.value)">
</div>
<input type="hidden" id="cardCode">
<input type="hidden" id="cardName">
</div>
<div class="field"><label>Vendor Ref. No. (Bilty No.)</label><input type="text" id="numAtCard" placeholder="e.g. 6600" oninput="syncBilty(this.value)"></div>
<div class="field"><label>ARNO</label><input type="text" id="arnoHeader" placeholder="e.g. 626030507"></div>
</div>
<!-- META: removed Series, removed Branch Reg (moved to branch change only) -->
<div class="meta-strip">
<div class="mi"><label>Doc No.</label><span id="docNumDisp">AUTO</span></div>
<div class="mi"><label>Status</label><span id="statusDisp">Draft</span></div>
<div class="mi">
<label>Posting Date <span class="req">*</span></label>
<input type="date" id="docDate" style="font-family:'DM Mono',monospace;font-size:11px;color:var(--accent2);border:none;background:transparent;padding:0;width:100px;">
</div>
<div class="mi">
<label>Due Date</label>
<input type="date" id="docDueDate" style="font-family:'DM Mono',monospace;font-size:11px;color:var(--accent2);border:none;background:transparent;padding:0;width:100px;">
</div>
<div class="mi">
<label>Document Date</label>
<input type="date" id="taxDate" style="font-family:'DM Mono',monospace;font-size:11px;color:var(--accent2);border:none;background:transparent;padding:0;width:100px;">
</div>
</div>
</div>
<hr class="dv" style="margin:8px 0;">
<div class="g2" style="max-width:400px;">
<div class="field">
<label>Branch <span class="req">*</span></label>
<select id="bplId"><option value="">— Select —</option></select>
</div>
<div class="field">
<span style="font-size:9px;color:var(--text-muted);font-family:'DM Mono',monospace;display:block;margin-bottom:4px;">REG. NO.</span>
<span id="branchRegDisp" style="font-family:'DM Mono',monospace;font-size:11px;color:var(--accent2);">—</span>
</div>
</div>
</div>
<!-- TABS CARD -->
<div class="tcard">
<div class="tabs">
<div class="tab active" onclick="switchTab(this,'tc')">Contents</div>
<div class="tab" onclick="switchTab(this,'tat')">Attachments</div>
</div>
<div class="tab-pane active" id="tc">
<div class="lines-bar">
<div style="display:flex;align-items:center;gap:8px;">
<span class="stitle">Document Lines</span>
<div style="display:flex;align-items:center;gap:4px;">
<span style="font-size:10px;color:var(--text-muted);">Item/Service Type</span>
<select id="lineType" style="width:auto;padding:3px 20px 3px 7px;font-size:11px;" onchange="rebuildLines()">
<option value="Service">Service</option>
<option value="Items">Items</option>
</select>
</div>
</div>
<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" id="linesWrap">
<table id="linesTable">
<thead id="lhead"><tr></tr></thead>
<tbody id="tbody"></tbody>
</table>
</div>
<div class="totals-wrap">
<div class="tg">
<span class="tl">Total Before Discount</span><span class="tv" id="totPre">₹ 0.00</span>
<span class="tl">Discount %</span><span class="tv"><input type="number" id="discPct" value="0" min="0" max="100" step="0.01" oninput="recalc()"></span>
<span class="tl">Freight</span><span class="tv"><input type="number" id="freightAmt" value="0" min="0" oninput="recalc()"></span>
<span class="tl">Rounding</span><span class="tv"><input type="number" id="roundAmt" value="0" oninput="recalc()"></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 style="display:grid;grid-template-columns:auto 1fr;gap:20px;align-items:start;padding:10px 2px 4px;">
<div>
<div class="g2" style="margin-bottom:6px;">
<div class="field"><label>Buyer</label><select id="buyer" style="width:160px;"><option value="">— Select —</option></select></div>
<div class="field"><label>Owner</label><input type="text" id="owner" readonly style="width:160px;"></div>
</div>
</div>
<div class="field" style="max-width:300px;">
<label>Remarks</label>
<textarea id="comments" rows="2" placeholder="e.g. BILTY NO 6600"></textarea>
</div>
</div>
</div>
<div class="tab-pane" id="tat">
<div style="font-size:9px;color:var(--text-muted);margin-bottom:10px;font-family:'DM Mono',monospace;">
Files are uploaded to the SAP B1 attachment path and linked to the GRPO via <code>AttachmentEntry</code>.
Max 5MB per file. PDF, JPG, PNG accepted.
</div>
<div class="att-grid">
<div>
<div class="att-label">Bilty / LR Copy</div>
<div class="attach-zone" id="az-bilty"
ondragover="event.preventDefault();this.classList.add('drag')"
ondragleave="this.classList.remove('drag')"
ondrop="event.preventDefault();this.classList.remove('drag');grpoHandleAttach('bilty',{files:event.dataTransfer.files})">
<input type="file" accept=".pdf,.jpg,.jpeg,.png" multiple onchange="grpoHandleAttach('bilty',this)">
<div class="az-icon"></div>
<div class="az-label">Click or drag Bilty / LR</div>
<div class="az-sub">PDF, JPG, PNG — max 5MB</div>
</div>
<div id="ap-bilty"></div>
</div>
<div>
<div class="att-label">Invoice Copy</div>
<div class="attach-zone" id="az-invoice"
ondragover="event.preventDefault();this.classList.add('drag')"
ondragleave="this.classList.remove('drag')"
ondrop="event.preventDefault();this.classList.remove('drag');grpoHandleAttach('invoice',{files:event.dataTransfer.files})">
<input type="file" accept=".pdf,.jpg,.jpeg,.png" multiple onchange="grpoHandleAttach('invoice',this)">
<div class="az-icon"></div>
<div class="az-label">Click or drag Invoice</div>
<div class="az-sub">PDF, JPG, PNG — max 5MB</div>
</div>
<div id="ap-invoice"></div>
</div>
<div>
<div class="att-label">Weighbridge / Quality Report</div>
<div class="attach-zone" id="az-quality"
ondragover="event.preventDefault();this.classList.add('drag')"
ondragleave="this.classList.remove('drag')"
ondrop="event.preventDefault();this.classList.remove('drag');grpoHandleAttach('quality',{files:event.dataTransfer.files})">
<input type="file" accept=".pdf,.jpg,.jpeg,.png" multiple onchange="grpoHandleAttach('quality',this)">
<div class="az-icon"></div>
<div class="az-label">Click or drag Report</div>
<div class="az-sub">PDF, JPG, PNG — max 5MB</div>
</div>
<div id="ap-quality"></div>
</div>
<div>
<div class="att-label">Other Documents</div>
<div class="attach-zone" id="az-other"
ondragover="event.preventDefault();this.classList.add('drag')"
ondragleave="this.classList.remove('drag')"
ondrop="event.preventDefault();this.classList.remove('drag');grpoHandleAttach('other',{files:event.dataTransfer.files})">
<input type="file" accept=".pdf,.jpg,.jpeg,.png" multiple onchange="grpoHandleAttach('other',this)">
<div class="az-icon"></div>
<div class="az-label">Click or drag Other</div>
<div class="az-sub">PDF, JPG, PNG — max 5MB</div>
</div>
<div id="ap-other"></div>
</div>
</div>
</div>
</div>
<pre class="resp" id="respPanel"></pre>
</div>
<!-- Fixed autocomplete portal -->
<div class="acd-fixed" id="acPortal"></div>
<script>
// ═══════════════════════════════════════════
// CONFIG
// ═══════════════════════════════════════════
const API = '';
const GRPO_ENDPOINT = '/api/sap/grpo';
let TOKEN = localStorage.getItem('grpo_token') || '';
let lineSeq = 0;
const _timers = {};
let _taxCodes = [];
let _branches = [];
let _slpList = [];
let _sacCodes = [];
const _grpoAttachments = {}; // { bilty: [{name,size,type,data},...], invoice: [...], ... }
let _portalTarget = null;
const ah = () => TOKEN ? {'Authorization': `Bearer ${TOKEN}`} : {};
const esc = s => String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
const fmt = n => '₹ '+parseFloat(n||0).toFixed(2);
// ── TOAST ─────────────────────────────────
function toast(msg, type='info'){
const el = document.createElement('div');
el.className = `toast t-${type}`;
el.innerHTML = `<span>${{ok:'✓',err:'✕',info:'ℹ'}[type]||'ℹ'}</span><span>${esc(String(msg))}</span>`;
document.getElementById('ta').appendChild(el);
setTimeout(() => el.remove(), 5000);
}
// ── AUTH ──────────────────────────────────
async function doLogin(){
const u = document.getElementById('lu').value.trim();
const p = document.getElementById('lp').value;
const eEl = document.getElementById('loginErr');
eEl.style.display = 'none';
if(!u||!p){ eEl.textContent='Username and password required.'; eEl.style.display='block'; return; }
try{
const res = await fetch(`${API}/api/auth/login`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:u,password:p})});
const data = await res.json();
if(!res.ok||!data.success) throw new Error(data.message||'Login failed');
TOKEN = data.token;
localStorage.setItem('grpo_token', TOKEN);
document.getElementById('loginModal').classList.remove('show');
document.getElementById('userChip').textContent = data.user?.name || data.user?.username || '';
toast('Signed in','ok');
initPage();
}catch(err){ eEl.textContent=err.message; eEl.style.display='block'; }
}
document.getElementById('lp').addEventListener('keydown', e => { if(e.key==='Enter') doLogin(); });
(async()=>{
if(!TOKEN) return;
try{
const res = await fetch(`${API}/api/auth/me`,{headers:ah()});
const d = await res.json();
if(d.success){
document.getElementById('loginModal').classList.remove('show');
document.getElementById('userChip').textContent = d.user?.name || d.user?.username || '';
initPage();
} else { TOKEN=''; localStorage.removeItem('grpo_token'); }
}catch{ TOKEN=''; localStorage.removeItem('grpo_token'); }
})();
// ── INIT ──────────────────────────────────
// FIX: addLine() is called AFTER await loadTaxCodes() so tax selects populate correctly
async function initPage(){
const today = new Date().toISOString().slice(0,10);
['docDate','docDueDate','taxDate'].forEach(id => {
const el = document.getElementById(id);
if(el && !el.value) el.value = today;
});
buildHeaders();
loadBranches();
loadSacCodes();
await loadTaxCodes(); // ← MUST complete before addLine()
if(!document.querySelectorAll('#tbody tr').length) addLine();
loadBuyers();
}
// ── TABS ──────────────────────────────────
function switchTab(el, pid){
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
el.classList.add('active');
document.getElementById(pid).classList.add('active');
}
// ═══════════════════════════════════════════
// LOOKUPS
// ═══════════════════════════════════════════
async function loadSacCodes(){
try{
const res = await fetch(`${API}/api/sap/lookup/sac-codes`,{headers:ah()});
const d = await res.json();
_sacCodes = d.data || [];
console.log(`[GRPO] SAC codes loaded: ${_sacCodes.length}`);
// refresh SAC selects in any already-rendered lines
document.querySelectorAll('.sac-sel').forEach(sel => populateSacSelect(sel));
}catch(e){ console.warn('[GRPO] loadSacCodes failed:',e.message); }
}
function populateSacSelect(sel){
const cur = sel.value;
sel.innerHTML = '<option value="">— SAC —</option>' +
_sacCodes.map(s=>`<option value="${s.AbsEntry}"${String(s.AbsEntry)===String(cur)?' selected':''}>${esc(s.SACCode)} – ${esc(s.SACName)}</option>`).join('');
}
async function loadBranches(){
try{
const res = await fetch(`${API}/api/sap/lookup/branches`,{headers:ah()});
const d = await res.json();
_branches = d.data || [];
const sel = document.getElementById('bplId');
sel.innerHTML = '<option value="">— Select —</option>' + _branches.map(b=>`<option value="${esc(b.BPLId)}">${esc(b.BPLName)}</option>`).join('');
const factory = _branches.find(b => b.BPLName.toUpperCase().includes('FACTORY'));
if(factory){ sel.value = String(factory.BPLId); document.getElementById('branchRegDisp').textContent = factory.TaxIdNum||'—'; }
// Branch change only updates Reg. No. display — shipFrom NOT touched
sel.addEventListener('change', ()=>{
const b = _branches.find(x => String(x.BPLId)===String(sel.value));
document.getElementById('branchRegDisp').textContent = b?.TaxIdNum || '—';
});
}catch(e){ console.warn('[GRPO] branches:', e.message); }
}
async function loadTaxCodes(){
try{
const res = await fetch(`${API}/api/sap/lookup/tax-codes`,{headers:ah()});
if(!res.ok) throw new Error(`HTTP ${res.status}`);
const d = await res.json();
if(d.data && d.data.length){
_taxCodes = d.data;
console.log('[GRPO] Tax codes loaded:', _taxCodes.length);
} else {
throw new Error(d.warning || 'No tax codes returned');
}
}catch(e){
console.warn('[GRPO] Tax code fetch failed, using fallback:', e.message);
// Hardcoded fallback so UI always has tax codes
_taxCodes = [
{Code:'CG+SG@0', Name:'CGST+SGST 0%', Rate:0},
{Code:'CG+SG@5', Name:'CGST+SGST 5%', Rate:5},
{Code:'CG+SG@12', Name:'CGST+SGST 12%', Rate:12},
{Code:'CG+SG@18', Name:'CGST+SGST 18%', Rate:18},
{Code:'CG+SG@28', Name:'CGST+SGST 28%', Rate:28},
{Code:'IGST@0', Name:'IGST 0%', Rate:0},
{Code:'IGST@5', Name:'IGST 5%', Rate:5},
{Code:'IGST@12', Name:'IGST 12%', Rate:12},
{Code:'IGST@18', Name:'IGST 18%', Rate:18},
{Code:'IGST@28', Name:'IGST 28%', Rate:28},
{Code:'EXEMPT', Name:'Exempt', Rate:0},
{Code:'NIL', Name:'NIL Rated', Rate:0},
{Code:'RIGST@5', Name:'RIGST 5%', Rate:5},
];
}
// Refresh any existing selects (e.g. after rebuild)
document.querySelectorAll('.taxcode-sel').forEach(s => fillTaxSel(s));
}
function fillTaxSel(sel, defaultCode){
const prev = defaultCode || sel.value;
sel.innerHTML = _taxCodes.map(t =>
`<option value="${esc(t.Code)}" data-rate="${t.Rate||0}">${esc(t.Code)}${t.Name&&t.Name!==t.Code?' — '+t.Name:''}</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 t = row.querySelector('.taxpct-inp');
if(t){ t.value = rate; recalc(); }
}
};
}
async function loadBuyers(){
try{
const res = await fetch(`${API}/api/sap/lookup/sales-employees`,{headers:ah()});
const d = await res.json();
_slpList = d.data || [];
const sel = document.getElementById('buyer');
sel.innerHTML = '<option value="">— Select —</option>' + _slpList.map(s=>`<option value="${s.SlpCode}">${esc(s.SlpName)}</option>`).join('');
const ginni = _slpList.find(s => s.SlpName.toUpperCase().includes('GINNI'));
if(ginni) sel.value = String(ginni.SlpCode);
}catch(e){}
}
// ═══════════════════════════════════════════
// 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(240, 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 AC ─────────────────────────────
function vendorAc(q){
clearTimeout(_timers.vendor);
if(!q||q.length<1){ closePortal(); return; }
openPortalLoading(document.getElementById('vendorSearch'));
_timers.vendor = setTimeout(async()=>{
try{
const res = await fetch(`${API}/api/sap/lookup/vendors?q=${encodeURIComponent(q)}`,{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); }
}, 280);
}
document.getElementById('vendorSearch').addEventListener('focus', function(){ if(this.value.length>0) vendorAc(this.value); });
// ── GL Account AC ─────────────────────────
function glAcForInput(inputEl, onSelect){
const q = inputEl.value;
clearTimeout(_timers['gl_'+inputEl.id]);
if(!q||q.length<1){ closePortal(); return; }
openPortalLoading(inputEl);
_timers['gl_'+inputEl.id] = setTimeout(async()=>{
try{
const res = await fetch(`${API}/api/sap/lookup/gl-accounts?q=${encodeURIComponent(q)}`,{headers:ah()});
const data = await res.json();
const list = (data.data||[]).map(a=>({label:a.AcctName,code:a.AcctCode,raw:a}));
if(!list.length){ openPortalMsg(inputEl,'No accounts found'); return; }
openPortal(inputEl, list, item => onSelect(item.raw.AcctCode, item.raw.AcctName));
}catch(err){ openPortalMsg(inputEl,'Error: '+err.message); }
}, 260);
}
// ── Costing Code AC ───────────────────────
function ccAcForInput(inputEl, dim, onSelect){
const q = inputEl.value;
clearTimeout(_timers['cc_'+inputEl.id]);
if(!q||q.length<1){ closePortal(); return; }
openPortalLoading(inputEl);
_timers['cc_'+inputEl.id] = setTimeout(async()=>{
try{
const res = await fetch(`${API}/api/sap/lookup/costing-codes?q=${encodeURIComponent(q)}&dim=${dim}`,{headers:ah()});
const data = await res.json();
const list = (data.data||[]).map(c=>({label:c.PrcName||c.PrcCode,code:c.PrcCode,raw:c}));
if(!list.length){ openPortalMsg(inputEl,'No costing codes found'); return; }
openPortal(inputEl, list, item => onSelect(item.raw.PrcCode, item.raw.PrcName));
}catch(err){ openPortalMsg(inputEl,'Error: '+err.message); }
}, 240);
}
// ── Customer AC ───────────────────────────
function custAcForInput(inputEl, onSelect){
const q = inputEl.value;
clearTimeout(_timers['cust_'+inputEl.id]);
if(!q||q.length<1){ closePortal(); return; }
openPortalLoading(inputEl);
_timers['cust_'+inputEl.id] = setTimeout(async()=>{
try{
const res = await fetch(`${API}/api/sap/lookup/customers?q=${encodeURIComponent(q)}`,{headers:ah()});
const data = await res.json();
const list = (data.data||[]).map(c=>({label:c.CardName,code:c.CardCode,raw:c}));
if(!list.length){ openPortalMsg(inputEl,'No customers found'); return; }
openPortal(inputEl, list, item => onSelect(item.raw.CardCode, item.raw.CardName));
}catch(err){ openPortalMsg(inputEl,'Error: '+err.message); }
}, 240);
}
// ── Item AC ───────────────────────────────
function itemAcForInput(inputEl, onSelect){
const q = inputEl.value;
clearTimeout(_timers['item_'+inputEl.id]);
if(!q||q.length<1){ closePortal(); return; }
openPortalLoading(inputEl);
_timers['item_'+inputEl.id] = setTimeout(async()=>{
try{
const res = await fetch(`${API}/api/sap/lookup/items?q=${encodeURIComponent(q)}`,{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); }
}, 260);
}
// ═══════════════════════════════════════════
// TABLE HEADERS
//
// CORRECTED DIMENSION MAPPING (from SAP ODIM table — Image 1):
// Dim1 → CostingCode = Variety
// Dim2 → CostingCode2 = Effective Month
// Dim3 → CostingCode3 = Budget
// Dim4 → CostingCode4 = Sub Budget
// Dim5 → CostingCode5 = State
//
// Location column REMOVED — not needed
// ═══════════════════════════════════════════
function buildHeaders(){
const isService = document.getElementById('lineType').value === 'Service';
const thead = document.getElementById('lhead');
const cols = isService ? [
{h:'#', w:'28px'},
{h:'Description', w:'130px'},
{h:'G/L Account', w:'95px'},
{h:'G/L Name', w:'170px'},
{h:'Tax Code', w:'130px'},
{h:'Total (LC)', w:'90px', nc:1},
{h:'Variety', w:'90px'}, // Dim1 → CostingCode
{h:'Eff. Month', w:'90px'}, // Dim2 → CostingCode2
{h:'Budget', w:'90px'}, // Dim3 → CostingCode3
{h:'Sub Budget', w:'80px'}, // Dim4 → CostingCode4
{h:'SAC Code', w:'160px'},
{h:'Remarks', w:'120px'}, // U_Remarks
{h:'Scheme Against', w:'110px'},
{h:'Sub Account', w:'90px'}, // U_Sub_Account
{h:'Customer Code', w:'110px'}, // U_CardCode
{h:'ARNO', w:'95px'}, // U_ARNO
{h:'Bilty No.', w:'90px'}, // U_BilltyNumber
{h:'PURPOSE', w:'100px'}, // U_Purpose
{h:'Unit Price', w:'90px', nc:1},
{h:'Tax %', w:'50px', nc:1},
{h:'Litres', w:'65px', nc:1},// U_UNE_LTS
{h:'', w:'26px'},
] : [
{h:'#', w:'28px'},
{h:'Item Code', w:'90px'},
{h:'Description', w:'140px'},
{h:'Warehouse', w:'75px'},
{h:'Tax Code', w:'130px'},
{h:'Unit Price', w:'90px', nc:1},
{h:'Qty', w:'55px', nc:1},
{h:'Total (LC)', w:'90px', nc:1},
{h:'Tax %', w:'50px', nc:1},
{h:'Bilty No', w:'80px'},
{h:'ARNO', w:'95px'},
{h:'Sub Account', w:'90px'},
{h:'Customer Code', w:'110px'},
{h:'Litres', w:'65px', nc:1},
{h:'', w:'26px'},
];
thead.innerHTML = '<tr>' + cols.map(c =>
`<th style="min-width:${c.w};width:${c.w};"${c.nc?' class="nc"':''}>${c.h}</th>`
).join('') + '</tr>';
}
// ═══════════════════════════════════════════
// ADD LINE
// ═══════════════════════════════════════════
function addLine(d={}){
lineSeq++;
const lid = lineSeq;
const isService = document.getElementById('lineType').value === 'Service';
const biltyMain = document.getElementById('numAtCard').value || '';
const tr = document.createElement('tr');
tr.id = `line-${lid}`;
const subOpts = ['SALES','SALES RETURN','BST'].map(o=>`<option value="${o}"${(d.subAcct||'SALES')===o?' selected':''}>${o}</option>`).join('');
const purpOpts = ['SALE','RETURNABLE','CONSUMABLE','BST',''].map(o=>`<option value="${o}"${(d.purpose||(o==='SALE'?'SALE':''))===o?' selected':''}>${o||'—'}</option>`).join('');
const rowNum = document.querySelectorAll('#tbody tr').length + 1;
if(isService){
tr.innerHTML = `
<td><span class="li">${rowNum}</span></td>
<td style="min-width:130px;"><input class="desc-inp" type="text" id="desc-${lid}" placeholder="Description" value="${esc(d.desc||'')}"></td>
<td style="min-width:95px;">
<input class="gl-inp" type="text" id="gl-${lid}" placeholder="G/L Acct" value="${esc(d.gl||'')}"
oninput="glAcForInput(this,function(c,n){selGL(${lid},c,n)})"
onfocus="if(this.value.length>0)glAcForInput(this,function(c,n){selGL(${lid},c,n)})">
</td>
<td style="min-width:170px;"><span class="gl-name" id="glname-${lid}" style="font-size:10px;color:var(--text-muted);padding:5px 6px;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:165px;" title="">${esc(d.glName||'')}</span></td>
<td style="min-width:130px;"><select class="taxcode-sel" id="tax-${lid}"></select></td>
<td class="nc" style="min-width:90px;"><input class="total-inp" type="number" readonly value="${d.price||0}" style="color:var(--accent2);font-weight:600;"></td>
<!-- Dim1 = Variety → CostingCode -->
<td style="min-width:90px;">
<input class="cc1-inp" type="text" id="cc1-${lid}" placeholder="Variety"
value="${esc(d.variety||'')}"
oninput="ccAcForInput(this,1,function(c){selCC1Variety(${lid},c)})"
onfocus="if(this.value.length>0)ccAcForInput(this,1,function(c){selCC1Variety(${lid},c)})">
</td>
<!-- Dim2 = Effective Month → CostingCode2 -->
<td style="min-width:90px;">
<input class="cc2-inp" type="text" id="cc2-${lid}" placeholder="04-2026"
value="${esc(d.effMonth||'')}"
oninput="ccAcForInput(this,2,function(c){selCC(${lid},'cc2',c)})"
onfocus="if(this.value.length>0)ccAcForInput(this,2,function(c){selCC(${lid},'cc2',c)})">
</td>
<!-- Dim3 = Budget → CostingCode3 -->
<td style="min-width:90px;">
<input class="cc3-inp" type="text" id="cc3-${lid}" placeholder="Budget"
value="${esc(d.budget||'')}"
oninput="ccAcForInput(this,3,function(c){selCC(${lid},'cc3',c)})"
onfocus="if(this.value.length>0)ccAcForInput(this,3,function(c){selCC(${lid},'cc3',c)})">
</td>
<!-- Dim4 = Sub Budget → CostingCode4 -->
<td style="min-width:80px;"><input class="cc4-inp" type="text" id="cc4-${lid}" placeholder="Sub Budget" value="${esc(d.subBudget||'')}"></td>
<td style="min-width:160px;"><select class="sac-sel" id="sac-${lid}"><option value="">— SAC —</option></select></td>
<td style="min-width:120px;"><input class="remarks-inp" type="text" id="rem-${lid}" placeholder="BILTY NO" value="${esc(d.remarks||(biltyMain?'BILTY NO '+biltyMain:''))}"></td>
<td style="min-width:110px;"><input class="scheme-inp" type="text" id="scheme-${lid}" placeholder="From Variety" readonly style="background:var(--surface2);color:var(--text-muted);cursor:default;" value="${esc(d.scheme||'')}"></td>
<td style="min-width:90px;"><select class="subacct-sel" id="subacct-${lid}">${subOpts}</select></td>
<td style="min-width:110px;">
<input class="cust-inp" type="text" id="cust-${lid}" placeholder="ORGC000011" value="${esc(d.custCode||'')}"
oninput="custAcForInput(this,function(c){selCust(${lid},c)})"
onfocus="if(this.value.length>0)custAcForInput(this,function(c){selCust(${lid},c)})">
</td>
<td style="min-width:95px;"><input class="arno-inp" type="text" id="arno-${lid}" placeholder="626030507" value="${esc(d.arno||'')}"></td>
<td style="min-width:90px;"><input class="bilty-inp" type="text" id="bilty-${lid}" placeholder="${esc(biltyMain||'6600')}" value="${esc(d.bilty||biltyMain)}"></td>
<td style="min-width:100px;"><select class="purpose-sel" id="purp-${lid}">${purpOpts}</select></td>
<td class="nc" style="min-width:90px;"><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:50px;"><input class="taxpct-inp" type="number" id="tp-${lid}" value="${d.taxPct||0}" min="0" max="100" oninput="recalc()"></td>
<td class="nc" style="min-width:65px;"><input class="litres-inp" type="number" id="litres-${lid}" value="${d.litres||0}" min="0" step="0.001"></td>
<td><span class="xbtn" onclick="removeLine(${lid})">✕</span></td>`;
} else {
tr.innerHTML = `
<td><span class="li">${rowNum}</span></td>
<td style="min-width:90px;">
<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>0)itemAcForInput(this,function(i){selItem(${lid},i)})">
</td>
<td style="min-width:140px;"><input class="desc-inp" type="text" id="desc-${lid}" placeholder="Description" value="${esc(d.desc||'')}"></td>
<td style="min-width:75px;"><input class="whs-inp" type="text" id="whs-${lid}" placeholder="WH-F" value="${esc(d.whs||'')}"></td>
<td style="min-width:130px;"><select class="taxcode-sel" id="tax-${lid}"></select></td>
<td class="nc" style="min-width:90px;"><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:55px;"><input class="qty-inp" type="number" id="qty-${lid}" value="${d.qty||0}" min="0" step="0.001" oninput="recalc()"></td>
<td class="nc" style="min-width:90px;"><input class="total-inp" type="number" readonly value="0" style="color:var(--accent2);font-weight:600;"></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()"></td>
<td style="min-width:80px;"><input class="bilty-inp" type="text" id="bilty-${lid}" placeholder="6600" value="${esc(d.bilty||biltyMain)}"></td>
<td style="min-width:95px;"><input class="arno-inp" type="text" id="arno-${lid}" placeholder="626030507" value="${esc(d.arno||'')}"></td>
<td style="min-width:90px;"><select class="subacct-sel" id="subacct-${lid}">${subOpts}</select></td>
<td style="min-width:110px;">
<input class="cust-inp" type="text" id="cust-${lid}" placeholder="Customer Code" value="${esc(d.custCode||'')}"
oninput="custAcForInput(this,function(c){selCust(${lid},c)})"
onfocus="if(this.value.length>0)custAcForInput(this,function(c){selCust(${lid},c)})">
</td>
<td class="nc" style="min-width:65px;"><input class="litres-inp" type="number" id="litres-${lid}" value="${d.litres||0}" min="0" step="0.001"></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);
if(d.taxCode) taxSel.value = d.taxCode;
}
// Populate SAC select (service lines only)
const sacSel = tr.querySelector('.sac-sel');
if(sacSel){
populateSacSelect(sacSel);
if(d.sacEntry) sacSel.value = String(d.sacEntry);
}
renumber(); recalc();
}
// ── Selection helpers ─────────────────────
function selGL(lid, code, name){
const inp = document.getElementById(`gl-${lid}`);
const span = document.getElementById(`glname-${lid}`);
if(inp) inp.value = code;
if(span){ span.textContent = name; span.title = name; }
}
function selCC(lid, field, code){
const inp = document.getElementById(`${field}-${lid}`);
if(inp) inp.value = code;
}
// Variety (Dim1 = cc1) also syncs Scheme Against
function selCC1Variety(lid, code){
const inp = document.getElementById(`cc1-${lid}`);
const scheme = document.getElementById(`scheme-${lid}`);
if(inp) inp.value = code;
if(scheme) scheme.value = code;
}
function selCust(lid, code){
const inp = document.getElementById(`cust-${lid}`);
if(inp) inp.value = code;
}
function selItem(lid, item){
const icode = document.getElementById(`icode-${lid}`);
const desc = document.getElementById(`desc-${lid}`);
if(icode) icode.value = item.ItemCode;
if(desc) desc.value = item.ItemName;
recalc();
}
// Sync Scheme Against when Variety (cc1) typed manually
document.getElementById('tbody').addEventListener('input', function(e){
if(e.target.classList.contains('cc1-inp')){
const row = e.target.closest('tr');
if(!row) return;
const id = row.id.replace('line-','');
const scheme = document.getElementById(`scheme-${id}`);
if(scheme) scheme.value = e.target.value;
}
});
function removeLine(lid){ document.getElementById(`line-${lid}`)?.remove(); renumber(); recalc(); }
function clearLines(){ 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; }); }
function rebuildLines(){
const n = Math.max(document.querySelectorAll('#tbody tr').length, 1);
buildHeaders();
document.getElementById('tbody').innerHTML = '';
lineSeq = 0;
for(let i=0; i<n; i++) addLine();
}
// ── Sync Bilty ────────────────────────────
function syncBilty(v){
document.querySelectorAll('.bilty-inp').forEach(i => i.value = v);
document.querySelectorAll('.remarks-inp').forEach(i => i.value = v ? `BILTY NO ${v}` : '');
const cm = document.getElementById('comments');
if(cm && !cm.dataset.manual) cm.value = v ? `BILTY NO ${v}` : '';
}
document.getElementById('comments').addEventListener('input', function(){ this.dataset.manual='1'; });
// ═══════════════════════════════════════════
// RECALCULATE
// ═══════════════════════════════════════════
function recalc(){
let pre=0, tax=0;
const isService = document.getElementById('lineType').value === 'Service';
document.querySelectorAll('#tbody tr').forEach(tr=>{
const price = parseFloat(tr.querySelector('.price-inp')?.value || 0);
const qty = parseFloat(tr.querySelector('.qty-inp')?.value || 1);
const taxPct = parseFloat(tr.querySelector('.taxpct-inp')?.value|| 0);
const lt = isService ? price * (qty>1?qty:1) : price * qty;
const ta = lt * taxPct / 100;
const totInp = tr.querySelector('.total-inp');
if(totInp) totInp.value = lt.toFixed(4);
pre += lt; tax += ta;
});
const disc = parseFloat(document.getElementById('discPct').value || 0);
const freight = parseFloat(document.getElementById('freightAmt').value || 0);
const rounding = parseFloat(document.getElementById('roundAmt').value || 0);
const tot = pre - pre*disc/100 + freight + rounding + tax;
document.getElementById('totPre').textContent = fmt(pre);
document.getElementById('totTax').textContent = fmt(tax);
document.getElementById('totDoc').textContent = fmt(tot);
}
// ═══════════════════════════════════════════
// BUILD PAYLOAD
//
// CORRECTED dimension mapping:
// CostingCode = Dim1 → Variety (cc1-inp)
// CostingCode2 = Dim2 → Eff. Month (cc2-inp)
// CostingCode3 = Dim3 → Budget (cc3-inp)
// CostingCode4 = Dim4 → Sub Budget (cc4-inp)
// ═══════════════════════════════════════════
// ATTACHMENT HANDLING
// ═══════════════════════════════════════════
function grpoHandleAttach(key, input){
Array.from(input.files || []).forEach(file => {
if(file.size > 5 * 1024 * 1024){ toast(file.name + ' exceeds 5 MB', 'err'); return; }
const r = new FileReader();
r.onload = e => {
if(!_grpoAttachments[key]) _grpoAttachments[key] = [];
_grpoAttachments[key].push({ name: file.name, size: file.size, type: file.type, data: e.target.result });
renderGrpoAttachPreviews(key);
};
r.readAsDataURL(file);
});
if(input.value !== undefined) input.value = '';
}
function renderGrpoAttachPreviews(key){
const el = document.getElementById('ap-' + key);
if(!el) return;
const arr = _grpoAttachments[key] || [];
el.innerHTML = arr.map((a, i) => `<div class="attach-preview">
<div class="ap-thumb">${a.type?.startsWith('image/') ? `<img src="${a.data}">` : a.type==='application/pdf'?'':''}</div>
<div class="ap-info"><div class="ap-name">${esc(a.name)}</div><div class="ap-size">${(a.size/1024).toFixed(1)} KB</div></div>
<button class="ap-rm" onclick="grpoRemoveAttach('${key}',${i})">✗</button>
</div>`).join('');
}
function grpoRemoveAttach(key, idx){
if(_grpoAttachments[key]) _grpoAttachments[key].splice(idx, 1);
renderGrpoAttachPreviews(key);
}
function grpoHasAttachments(){
return Object.values(_grpoAttachments).some(arr => arr?.length > 0);
}
// ═══════════════════════════════════════════
// CostingCode5 = Dim5 → State (cc5-inp)
//
// LocationCode is required by SAP (OLCT.AbsEntry) — sent on every line.
// Series is NOT sent — SAP auto-assigns based on document date/branch.
// ═══════════════════════════════════════════
function buildPayload(){
const cardCode = document.getElementById('cardCode').value.trim();
const docDate = document.getElementById('docDate').value;
const dueDateV = document.getElementById('docDueDate').value;
const taxDateV = document.getElementById('taxDate').value;
const isService = document.getElementById('lineType').value === 'Service';
const biltyMain = document.getElementById('numAtCard').value;
const buyerCode = parseInt(document.getElementById('buyer').value) || null;
const bplId = parseInt(document.getElementById('bplId').value) || null;
const disc = parseFloat(document.getElementById('discPct').value || 0);
// LocationCode = BPL ID (branch) — OLCT.AbsEntry matches BPL_IDAssignedToInvoice
const locationCode = bplId || null;
const arnoHeader = document.getElementById('arnoHeader')?.value?.trim() || '';
const lines = [];
document.querySelectorAll('#tbody tr').forEach((tr, idx)=>{
const price = parseFloat(tr.querySelector('.price-inp')?.value || 0);
const qty = parseFloat(tr.querySelector('.qty-inp')?.value || 1);
const taxPct = parseFloat(tr.querySelector('.taxpct-inp')?.value || 0);
const taxCode = tr.querySelector('.taxcode-sel')?.value || '';
const litres = parseFloat(tr.querySelector('.litres-inp')?.value || 0);
if(isService){
const variety = tr.querySelector('.cc1-inp')?.value || ''; // Dim1
const effMonth = tr.querySelector('.cc2-inp')?.value || ''; // Dim2
const budget = tr.querySelector('.cc3-inp')?.value || ''; // Dim3
const subBudget = tr.querySelector('.cc4-inp')?.value || ''; // Dim4
const remInp = tr.querySelector('.remarks-inp')?.value || '';
const sacEntry = parseInt(tr.querySelector('.sac-sel')?.value) || null;
const subAcct = tr.querySelector('.subacct-sel')?.value || '';
const custCode = tr.querySelector('.cust-inp')?.value?.trim() || '';
const arno = tr.querySelector('.arno-inp')?.value?.trim() || '';
const biltyNo = tr.querySelector('.bilty-inp')?.value?.trim() || '';
const purpose = tr.querySelector('.purpose-sel')?.value || '';
const line = {
LineNum: idx,
ItemDescription: tr.querySelector('.desc-inp')?.value || '',
AccountCode: tr.querySelector('.gl-inp')?.value || '',
Quantity: 1,
UnitPrice: price,
LineTotal: price,
TaxCode: taxCode,
VatGroup: taxCode,
FreeText: remInp,
};
if(variety) line.CostingCode = variety; // Dim1
if(effMonth) line.CostingCode2 = effMonth; // Dim2
if(budget) line.CostingCode3 = budget; // Dim3
if(subBudget) line.CostingCode4 = subBudget; // Dim4
if(sacEntry) line.SACEntry = sacEntry; // GST SAC (OSAC.AbsEntry)
// UDFs
if(litres > 0) line.U_UNE_LTS = litres; // litres → U_UNE_LTS
if(remInp) line.U_Remarks = remInp; // remarks → U_Remarks
if(subAcct) line.U_Sub_Account = subAcct; // sub account → U_Sub_Account
if(custCode) line.U_CardCode = custCode; // customer code → U_CardCode
if(arno) line.U_ARNO = arno; // ARNO → U_ARNO
if(biltyNo) line.U_BilltyNumber= biltyNo; // bilty no → U_BilltyNumber
if(purpose) line.U_Purpose = purpose; // purpose → U_Purpose
if(buyerCode) line.SalesPersonCode = buyerCode;
if(locationCode) line.LocationCode = locationCode;
lines.push(line);
} else {
const subAcct = tr.querySelector('.subacct-sel')?.value || '';
const custCode = tr.querySelector('.cust-inp')?.value?.trim() || '';
const arno = tr.querySelector('.arno-inp')?.value?.trim() || '';
const biltyNo = tr.querySelector('.bilty-inp')?.value?.trim() || '';
const line = {
LineNum: idx,
ItemCode: tr.querySelector('.icode-inp')?.value?.trim().toUpperCase() || '',
ItemDescription: tr.querySelector('.desc-inp')?.value || '',
WarehouseCode: tr.querySelector('.whs-inp')?.value || '',
Quantity: qty,
UnitPrice: price,
LineTotal: parseFloat((price*qty).toFixed(2)),
TaxCode: taxCode,
VatGroup: taxCode,
};
if(litres > 0) line.U_UNE_LTS = litres;
if(subAcct) line.U_Sub_Account = subAcct;
if(custCode) line.U_CardCode = custCode;
if(arno) line.U_ARNO = arno;
if(biltyNo) line.U_BilltyNumber = biltyNo;
if(buyerCode) line.SalesPersonCode = buyerCode;
if(locationCode) line.LocationCode = locationCode;
lines.push(line);
}
});
const payload = {
DocType: isService ? 'dDocument_Service' : 'dDocument_Items',
CardCode: cardCode,
NumAtCard: biltyMain,
DocDate: docDate ? `${docDate}T00:00:00Z` : undefined,
DocDueDate: dueDateV ? `${dueDateV}T00:00:00Z` : undefined,
TaxDate: taxDateV ? `${taxDateV}T00:00:00Z` : undefined,
Comments: document.getElementById('comments').value || (biltyMain ? `BILTY NO ${biltyMain}` : ''),
DocumentLines: lines,
};
// Series NOT included — SAP auto-assigns
if(bplId) payload.BPL_IDAssignedToInvoice = bplId;
if(disc > 0) payload.DiscountPercent = disc;
if(buyerCode) payload.SalesPersonCode = buyerCode;
const freight = parseFloat(document.getElementById('freightAmt').value || 0);
if(freight > 0){
payload.DocumentAdditionalExpenses = [{
ExpenseCode: 1,
LineGross: freight,
LineTotal: freight,
}];
}
Object.keys(payload).forEach(k => { if(payload[k]===undefined) delete payload[k]; });
// Attach files (base64) if any were selected
if(grpoHasAttachments()) payload.attachments = _grpoAttachments;
return payload;
}
// ═══════════════════════════════════════════
// SUBMIT
// ═══════════════════════════════════════════
async function submitGRPO(){
if(!document.getElementById('cardCode').value.trim())
return toast('Select a vendor first.','err');
if(!document.getElementById('docDate').value)
return toast('Posting Date required.','err');
const rows = document.querySelectorAll('#tbody tr');
if(!rows.length)
return toast('Add at least one line.','err');
const isService = document.getElementById('lineType').value === 'Service';
if(isService){
let hasGL = true;
rows.forEach(tr => { if(!tr.querySelector('.gl-inp')?.value?.trim()) hasGL = false; });
if(!hasGL) return toast('All service lines need a G/L Account.','err');
}
const payload = buildPayload();
showResp(payload);
document.getElementById('overlay').classList.add('show');
try{
const res = await fetch(`${API}${GRPO_ENDPOINT}`, {
method: 'POST',
headers: {'Content-Type':'application/json', ...ah()},
body: JSON.stringify(payload),
});
const data = await res.json();
document.getElementById('overlay').classList.remove('show');
if(res.ok && data?.DocEntry){
document.getElementById('statusBadge').textContent = '● OPEN';
document.getElementById('statusBadge').className = 'badge badge-open';
document.getElementById('docNumDisp').textContent = data.DocNum || data.DocEntry || 'Posted';
document.getElementById('statusDisp').textContent = 'Open';
showResp(data);
toast(`✓ GRPO Posted — Doc# ${data.DocNum||data.DocEntry}`, 'ok');
} else {
const sapMsg = data?.error?.message?.value || data?.message || JSON.stringify(data);
toast(`Error: ${sapMsg}`, 'err');
showResp(data);
}
}catch(err){
document.getElementById('overlay').classList.remove('show');
toast('Network error: ' + err.message, 'err');
showResp({error: err.message, payload});
}
}
function resetForm(){
if(!confirm('Reset all fields?')) return;
['vendorSearch','cardCode','cardName','numAtCard','comments'].forEach(id => {
const el = document.getElementById(id);
if(el) el.value = '';
});
delete document.getElementById('comments').dataset.manual;
const today = new Date().toISOString().slice(0,10);
['docDate','docDueDate','taxDate'].forEach(id => {
const el = document.getElementById(id);
if(el) el.value = today;
});
clearLines(); addLine();
document.getElementById('statusBadge').textContent = '● DRAFT';
document.getElementById('statusBadge').className = 'badge badge-draft';
document.getElementById('docNumDisp').textContent = 'AUTO';
document.getElementById('statusDisp').textContent = 'Draft';
document.getElementById('respPanel').style.display = 'none';
closePortal();
toast('Form reset.', 'info');
}
function toggleResp(){
const p = document.getElementById('respPanel');
p.style.display = p.style.display==='block' ? 'none' : 'block';
if(p.style.display==='block') showResp(buildPayload());
}
function showResp(d){
const p = document.getElementById('respPanel');
p.textContent = JSON.stringify(d, null, 2);
p.style.display = 'block';
}
</script>
<script src="/sidebar.js?v=25"></script>
</body>
</html>