first commit
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s

This commit is contained in:
John
2026-09-23 17:31:02 +05:30
commit 69b4e68baf
51657 changed files with 3864077 additions and 0 deletions
+910
View File
@@ -0,0 +1,910 @@
<!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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
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>