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
+912
View File
@@ -0,0 +1,912 @@
<!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,maximum-scale=1.0"/>
<title>Purchase Request — SAP B1</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Bebas+Neue&display=swap" rel="stylesheet"/>
<style>
:root{--ink:#080e18;--ink2:#0d1626;--ink3:#111f35;--purple:#9f5afd;--purple2:#7c3aed;--blue:#1a6fff;--amber:#f5a623;--green:#0dbb8a;--red:#e53e3e;--teal:#047e7e;--border:#1c2e48;--border2:#243650;--text:#c8d6e8;--text2:#7a9ab8;--text3:#3d5470;--surface:#0f1d30;--surface2:#142338;--surface3:#0b1520;--radius:10px;--font:'Space Grotesk',sans-serif;--mono:'JetBrains Mono',monospace;--display:'Bebas Neue',sans-serif;--shadow:0 8px 32px rgba(0,0,0,.5)}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:var(--font);background:var(--ink);color:var(--text);min-height:100vh;-webkit-font-smoothing:antialiased}
nav{position:sticky;top:0;z-index:200;background:rgba(8,14,24,.95);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);padding:0 16px;height:50px;display:flex;align-items:center;justify-content:space-between;gap:8px}
.nav-logo{background:var(--teal);color:#fff;font-family:var(--display);font-size:14px;letter-spacing:2px;padding:3px 9px;border-radius:5px;flex-shrink:0}
.nav-title{font-size:13px;font-weight:700;color:var(--text);flex:1}
.nav-sub{font-size:10px;color:var(--text3);font-family:var(--mono)}
.nav-right{display:flex;align-items:center;gap:8px}
.co-sel{background:var(--surface2);border:1px solid var(--border2);color:var(--text);border-radius:6px;padding:4px 10px;font-size:11px;font-family:var(--mono);cursor:pointer;outline:none}
.co-sel option{background:var(--ink2)}
.btn-logout{padding:4px 10px;border-radius:5px;font-size:10px;font-weight:700;border:1px solid var(--border2);color:var(--text3);background:none;cursor:pointer;font-family:var(--font)}.btn-logout:hover{border-color:var(--red);color:var(--red)}
.user-chip{font-family:var(--mono);font-size:10px;color:var(--text3)}
main{padding:14px 16px 80px;max-width:1100px;margin:0 auto}
/* ── Tabs ── */
.tabs{display:flex;gap:4px;margin-bottom:14px;background:var(--surface3);border-radius:var(--radius);padding:4px}
.tab{padding:8px 16px;border-radius:7px;font-size:12px;font-weight:700;color:var(--text3);cursor:pointer;border:none;background:none;font-family:var(--font);transition:all .15s}
.tab:hover{color:var(--text2)}
.tab.on{background:var(--surface);color:var(--teal);border:1px solid var(--border2);box-shadow:0 2px 8px rgba(0,0,0,.3)}
/* ── Cards ── */
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:12px;overflow:hidden}
.card-hd{background:var(--surface3);padding:10px 14px;font-size:11px;font-weight:700;color:var(--text2);display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--border);text-transform:uppercase;letter-spacing:.5px}
.card-hd .ico{font-size:14px}
.card-hd .right{margin-left:auto;font-size:10px;font-weight:400;color:var(--text3)}
.card-bd{padding:14px}
/* ── Fields ── */
.fg{margin-bottom:11px}
.fl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:var(--text3);margin-bottom:4px;display:block}
.req{color:var(--teal)}
.fi{width:100%;padding:7px 10px;border:1.5px solid var(--border2);border-radius:7px;font-size:12px;font-family:var(--font);background:var(--surface2);color:var(--text);outline:none;height:34px;transition:border-color .15s}
.fi:focus{border-color:var(--teal);box-shadow:0 0 0 2px rgba(4,126,126,.15)}
.fi::placeholder{color:var(--text3)}
.fi[readonly]{opacity:.6;cursor:default}
select.fi{cursor:pointer;appearance:none;background:var(--surface2) 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='%233d5470'/%3E%3C/svg%3E") no-repeat right 10px center;padding-right:26px}
select.fi option{background:var(--surface2)}
textarea.fi{height:auto;min-height:60px;resize:vertical}
.fi-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.fi-row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}
.fi-row4{display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:10px}
@media(max-width:600px){.fi-row,.fi-row3,.fi-row4{grid-template-columns:1fr}}
/* ── Search dropdown ── */
.sw{position:relative}
.sdd{position:absolute;top:100%;left:0;right:0;background:var(--surface2);border:1.5px solid var(--teal);border-radius:7px;box-shadow:0 12px 40px rgba(0,0,0,.7);z-index:300;max-height:220px;overflow-y:auto;display:none;margin-top:3px}
.sdd.open{display:block}
.sdd-row{padding:8px 12px;cursor:pointer;border-bottom:1px solid var(--border);display:flex;gap:10px;align-items:center;transition:background .08s}
.sdd-row:last-child{border-bottom:none}
.sdd-row:hover{background:rgba(4,126,126,.12)}
.sdd-code{font-family:var(--mono);font-size:11px;color:var(--teal);font-weight:600;min-width:90px;flex-shrink:0}
.sdd-name{font-size:11px;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sdd-msg{padding:12px;text-align:center;color:var(--text3);font-size:11px;font-style:italic}
/* ── Line items table ── */
.lines-wrap{overflow-x:auto;border:1px solid var(--border);border-radius:8px}
table{width:100%;border-collapse:collapse;font-size:11px}
thead tr{background:var(--surface3)}
th{padding:7px 8px;text-align:left;font-size:9px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;border-bottom:1px solid var(--border)}
td{padding:0;border-bottom:1px solid var(--border);vertical-align:middle}
tbody tr:last-child td{border-bottom:none}
tbody tr:hover td{background:rgba(4,126,126,.04)}
td input,td select{background:transparent;border:none;padding:6px 7px;font-size:11px;color:var(--text);width:100%;font-family:var(--font);height:30px;outline:none}
td input:focus,td select:focus{background:rgba(4,126,126,.08);outline:none}
td input[readonly]{color:var(--text3)}
td select{appearance:none;cursor:pointer}
.nc{text-align:right}.nc input{text-align:right;font-family:var(--mono)}
.li-num{display:block;width:28px;text-align:center;font-family:var(--mono);font-size:10px;color:var(--text3);padding:6px 2px}
.xbtn{display:block;width:26px;text-align:center;color:var(--text3);cursor:pointer;padding:5px 2px;font-size:14px;transition:color .12s;line-height:1}.xbtn:hover{color:var(--red)}
.tfoot-row{background:var(--surface3);border-top:1px solid var(--border)}
.tfoot-row td{padding:6px 8px;font-size:10px;font-weight:700;color:var(--text3);font-family:var(--mono)}
/* ── Buttons ── */
.btn-bar{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.btn{padding:8px 18px;border-radius:8px;border:none;font-family:var(--font);font-size:12px;font-weight:700;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:6px}
.btn:disabled{opacity:.4;cursor:not-allowed}
.b-primary{background:var(--teal);color:#fff}.b-primary:hover:not(:disabled){background:#035b5b;box-shadow:0 4px 14px rgba(4,126,126,.4)}
.b-secondary{background:var(--surface2);color:var(--text2);border:1px solid var(--border2)}.b-secondary:hover{border-color:var(--teal);color:var(--teal)}
.b-danger{background:rgba(229,62,62,.1);color:var(--red);border:1px solid rgba(229,62,62,.25)}.b-danger:hover{background:var(--red);color:#fff}
.b-warn{background:rgba(245,166,35,.1);color:var(--amber);border:1px solid rgba(245,166,35,.25)}.b-warn:hover{background:rgba(245,166,35,.2)}
.b-green{background:rgba(13,187,138,.1);color:var(--green);border:1px solid rgba(13,187,138,.25)}.b-green:hover{background:rgba(13,187,138,.2)}
.b-blue{background:rgba(26,111,255,.1);color:var(--blue);border:1px solid rgba(26,111,255,.25)}.b-blue:hover{background:rgba(26,111,255,.2)}
.btn-sm{padding:5px 11px;font-size:10px;border-radius:6px}
/* ── Status badges ── */
.badge{display:inline-block;padding:2px 8px;border-radius:5px;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;font-family:var(--mono)}
.s-open{background:rgba(13,187,138,.12);color:var(--green);border:1px solid rgba(13,187,138,.25)}
.s-closed{background:rgba(61,84,112,.2);color:var(--text3);border:1px solid var(--border2)}
.s-cancelled{background:rgba(229,62,62,.12);color:var(--red);border:1px solid rgba(229,62,62,.25)}
.s-draft{background:rgba(245,166,35,.12);color:var(--amber);border:1px solid rgba(245,166,35,.25)}
/* ── PR List cards — square-tile grid, same pattern as work-order.html ── */
.pr-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:14px}
.pr-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px;cursor:pointer;transition:all .15s;aspect-ratio:1/1;display:flex;flex-direction:column;overflow:hidden}
.pr-card:hover{border-color:var(--teal);box-shadow:0 6px 20px rgba(0,0,0,.35);transform:translateY(-2px)}
.pr-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;flex-wrap:wrap}
.pr-docnum{font-family:var(--mono);font-size:12.5px;color:var(--teal);font-weight:700}
.pr-icon{width:34px;height:34px;border-radius:9px;background:rgba(6,182,212,.12);color:var(--teal);display:flex;align-items:center;justify-content:center;margin:8px 0;font-size:16px}
.pr-comment{font-size:11px;color:var(--text2);margin-bottom:6px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.pr-meta{font-size:10.5px;color:var(--text3);display:flex;flex-direction:column;gap:3px;margin-top:auto;padding-top:8px;border-top:1px solid var(--border)}
.pr-meta b{color:var(--text2)}
.pr-empty{text-align:center;padding:40px;color:var(--text3);font-size:13px}
/* ── Search bar ── */
.search-bar{display:flex;gap:8px;margin-bottom:12px}
.search-bar .fi{flex:1}
.search-bar select.fi{flex:0 0 160px}
/* ── Toast ── */
.toast-c{position:fixed;top:58px;right:12px;z-index:9999;display:flex;flex-direction:column;gap:6px;pointer-events:none}
.toast{padding:9px 14px;border-radius:8px;font-size:12px;font-weight:600;box-shadow:var(--shadow);animation:sIn .2s ease;max-width:320px;pointer-events:auto;display:flex;align-items:center;gap:8px}
@keyframes sIn{from{transform:translateX(110%);opacity:0}to{transform:translateX(0);opacity:1}}
.t-ok{background:var(--green);color:#fff}.t-err{background:var(--red);color:#fff}.t-info{background:var(--teal);color:#fff}
/* ── Spinner ── */
.sp{width:13px;height:13px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite;display:inline-block}
@keyframes spin{to{transform:rotate(360deg)}}
.full-sp{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:8000;align-items:center;justify-content:center;flex-direction:column;gap:12px}
.full-sp.show{display:flex}
.full-sp .sp{width:36px;height:36px;border-width:3px}
.full-sp p{font-size:12px;color:rgba(255,255,255,.6);font-family:var(--mono)}
/* ── Document detail modal ── */
.modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:500;display:none;align-items:flex-start;justify-content:center;padding:14px;overflow-y:auto}
.modal-bg.show{display:flex}
.modal{background:var(--ink2);border:1px solid var(--border2);border-radius:12px;width:100%;max-width:780px;margin:auto;box-shadow:0 24px 80px rgba(0,0,0,.7);animation:mIn .18s ease}
@keyframes mIn{from{transform:translateY(16px);opacity:0}to{transform:translateY(0);opacity:1}}
.modal-hd{padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;gap:10px}
.modal-hd-left{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.modal-close{width:28px;height:28px;border-radius:6px;border:1px solid var(--border);background:var(--surface);color:var(--text2);cursor:pointer;font-size:16px;line-height:1;display:flex;align-items:center;justify-content:center}.modal-close:hover{background:var(--red);color:#fff;border-color:var(--red)}
.modal-bd{padding:14px;max-height:75vh;overflow-y:auto}
.modal-tabs{display:flex;gap:3px;margin-bottom:12px;border-bottom:1px solid var(--border);padding-bottom:0}
.modal-tab{padding:7px 14px;font-size:11px;font-weight:600;color:var(--text3);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:all .12s}
.modal-tab.on{color:var(--teal);border-bottom-color:var(--teal)}
.modal-tab-pane{display:none}.modal-tab-pane.on{display:block}
.dt-grid{display:grid;grid-template-columns:1fr 1fr;gap:0}
.dt-row{display:flex;justify-content:space-between;align-items:center;padding:7px 10px;border-bottom:1px solid rgba(28,46,72,.5);font-size:12px}
.dt-row:last-child{border-bottom:none}
.dt-l{color:var(--text3);font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.3px}
.dt-v{color:var(--text);font-family:var(--mono);font-size:11px;text-align:right}
.modal-actions{display:flex;gap:8px;flex-wrap:wrap;padding:12px 16px;border-top:1px solid var(--border);background:var(--surface3);border-radius:0 0 12px 12px}
/* ── Line detail table inside modal ── */
.line-table{width:100%;border-collapse:collapse;font-size:11px;margin-top:8px}
.line-table th{background:var(--surface3);padding:6px 8px;text-align:left;font-size:9px;font-weight:700;color:var(--text3);text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid var(--border)}
.line-table td{padding:7px 8px;border-bottom:1px solid rgba(28,46,72,.4);color:var(--text2);vertical-align:top}
.line-table tbody tr:last-child td{border-bottom:none}
.line-table .num{text-align:right;font-family:var(--mono)}
/* ── Pagination ── */
.pg-bar{display:flex;align-items:center;gap:8px;margin-top:12px;font-size:11px;color:var(--text3)}
.pg-bar button{padding:4px 10px;border-radius:5px;border:1px solid var(--border2);background:var(--surface2);color:var(--text2);font-size:10px;cursor:pointer;font-family:var(--font)}.pg-bar button:hover:not(:disabled){border-color:var(--teal);color:var(--teal)}.pg-bar button:disabled{opacity:.35}
</style>
<link rel="stylesheet" href="/theme.css?v=3"/>
</head>
<body>
<div class="full-sp" id="fullSp"><div class="sp"></div><p id="spMsg">Processing…</p></div>
<div class="toast-c" id="toastC"></div>
<!-- NAV -->
<nav>
<div style="display:flex;align-items:center;gap:10px;min-width:0">
<span class="nav-logo">PR</span>
<div style="min-width:0">
<div class="nav-title">Purchase Request</div>
<div class="nav-sub">Purchase Requests · SAP B1</div>
</div>
</div>
<div class="nav-right">
<span class="user-chip" id="userChip"></span>
<select class="co-sel" id="coSel" onchange="onCo()">
</select>
<button class="btn-logout" onclick="logout()">Logout</button>
</div>
</nav>
<main>
<div class="tabs" style="justify-content:space-between">
<div style="display:flex;gap:4px">
<button class="tab on" id="tab-create" onclick="switchTab('create')">New Request</button>
<button class="tab" id="tab-view" onclick="switchTab('view');loadList()">View / Search</button>
</div>
<select class="fi" id="coSel" onchange="onCo()" style="width:auto;height:32px;padding:4px 10px;font-size:11px;font-family:var(--mono)">
</select>
</div>
<!-- ══ CREATE TAB ══ -->
<div id="pane-create">
<!-- Header -->
<div class="card">
<div class="card-hd"><span class="ico"></span>Purchase Request Header
<span class="right">All dates required · Vendor optional</span>
</div>
<div class="card-bd">
<div class="fi-row4" style="margin-bottom:10px">
<div class="fg">
<span class="fl">Posting Date <span class="req">*</span></span>
<input class="fi" type="date" id="docDate">
</div>
<div class="fg">
<span class="fl">Required By <span class="req">*</span></span>
<input class="fi" type="date" id="docDueDate">
</div>
<div class="fg">
<span class="fl">Vendor Ref. No.</span>
<input class="fi" type="text" id="numAtCard" placeholder="e.g. PR-2025-001">
</div>
<div class="fg">
<span class="fl">Status</span>
<input class="fi" type="text" value="Draft" readonly>
</div>
</div>
<div class="fi-row4" style="margin-bottom:10px">
<div class="fg sw" id="vendorWrap">
<span class="fl">Vendor (Optional)</span>
<input class="fi" type="text" id="vendorSearch" placeholder="Search vendor code or name…" autocomplete="off" oninput="vendorAc(this.value)">
<input type="hidden" id="cardCode">
<div class="sdd" id="vendorDd"></div>
</div>
<div class="fg">
<span class="fl">Department <span class="req">*</span></span>
<select class="fi" id="uDepart">
<option value="">— Select Department —</option>
</select>
</div>
<div class="fg">
<span class="fl">STS Required</span>
<select class="fi" id="uStsReqd">
<option value="No">No</option>
<option value="Yes">Yes</option>
</select>
</div>
<div class="fg">
<span class="fl">Remarks / Purpose <span class="req">*</span></span>
<textarea class="fi" id="comments" rows="2" placeholder="Enter purpose or remarks for this purchase request…"></textarea>
</div>
</div>
</div>
</div>
<!-- Lines -->
<div class="card">
<div class="card-hd"><span class="ico"></span>Document Lines
<span class="right">
<button class="btn b-secondary btn-sm" onclick="addLine()">+ Add Line</button>
<button class="btn b-secondary btn-sm" style="margin-left:4px" onclick="clearLines()">Clear</button>
</span>
</div>
<div class="card-bd" style="padding:0">
<div class="lines-wrap">
<table>
<thead>
<tr>
<th style="width:30px">#</th>
<th style="min-width:120px">Item Code <span class="req">*</span></th>
<th style="min-width:160px">Description</th>
<th style="width:90px">Required Date</th>
<th style="width:70px;text-align:right">Qty <span class="req">*</span></th>
<th style="width:50px">Unit</th>
<th style="width:100px;text-align:right">Unit Price</th>
<th style="min-width:110px">Warehouse</th>
<th style="width:28px"></th>
</tr>
</thead>
<tbody id="linesBody"></tbody>
<tfoot>
<tr class="tfoot-row">
<td colspan="4" style="padding:7px 8px;font-size:10px;color:var(--text3)">TOTALS</td>
<td class="nc" style="padding:7px 8px"><span id="totQty">0</span></td>
<td></td>
<td class="nc" style="padding:7px 8px"><span id="totAmt">0.00</span></td>
<td colspan="2"></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="btn-bar">
<button class="btn b-primary" id="btnPost" onclick="submitPR()">
<span class="sp" id="spPost" style="display:none"></span> ✓ Submit for Approval
</button>
<button class="btn b-secondary" onclick="resetForm()">↺ Reset</button>
</div>
</div><!-- /pane-create -->
<!-- ══ VIEW TAB ══ -->
<div id="pane-view" style="display:none">
<div class="card">
<div class="card-hd"><span class="ico"></span>Search Purchase Requests</div>
<div class="card-bd">
<div class="search-bar">
<input class="fi" type="text" id="srchQ" placeholder="Search by doc no., vendor, remarks…" oninput="debounceSearch()">
<select class="fi" id="srchStatus" onchange="loadList()">
<option value="">All Status</option>
<option value="bost_Open">Open</option>
<option value="bost_Close">Closed</option>
</select>
<button class="btn b-primary btn-sm" onclick="loadList()">Search</button>
</div>
</div>
</div>
<div id="listWrap">
<div class="pr-empty">Click "View / Search" to load purchase requests.</div>
</div>
<div class="pg-bar" id="pgBar" style="display:none">
<button id="pgPrev" onclick="pgMove(-1)" disabled>← Prev</button>
<span id="pgInfo">Page 1</span>
<button id="pgNext" onclick="pgMove(1)">Next →</button>
</div>
</div><!-- /pane-view -->
</main>
<!-- ══ APPROVAL REMARKS DIALOG ══ -->
<div class="modal-bg" id="approvalDialog" style="z-index:600">
<div class="modal" style="max-width:480px">
<div class="modal-hd">
<div class="modal-hd-left">
<span style="font-size:16px"></span>
<span style="font-weight:700;font-size:13px">Request for Document Generation</span>
</div>
</div>
<div class="modal-bd" style="padding:16px">
<p style="font-size:12px;color:var(--text2);margin-bottom:14px;line-height:1.6">
Generating this document requires the authorisation of other users.<br>
Enter remarks relevant for the authoriser and choose <strong>OK</strong>.
</p>
<table style="width:100%;border-collapse:collapse;font-size:12px;margin-bottom:14px">
<thead>
<tr style="background:var(--surface3)">
<th style="padding:7px 10px;text-align:left;color:var(--text3);font-weight:700;width:30px">#</th>
<th style="padding:7px 10px;text-align:left;color:var(--text3);font-weight:700">Approval Template</th>
<th style="padding:7px 10px;text-align:left;color:var(--text3);font-weight:700">Remarks</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid var(--border)">
<td style="padding:8px 10px;color:var(--text2)">1</td>
<td style="padding:8px 10px;color:var(--text)">Purchase Indent</td>
<td style="padding:6px 10px">
<input class="fi" type="text" id="approvalRemarks" placeholder="Enter remarks for approver…" style="height:30px;font-size:12px">
</td>
</tr>
</tbody>
</table>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="btn b-secondary" onclick="closeApprovalDialog()">Cancel</button>
<button class="btn b-primary" id="btnApprovalOk" onclick="confirmApproval()">OK</button>
</div>
</div>
</div>
</div>
<!-- ══ DOCUMENT DETAIL MODAL ══ -->
<div class="modal-bg" id="docModal">
<div class="modal">
<div class="modal-hd">
<div class="modal-hd-left">
<span style="font-weight:700;font-size:13px">Purchase Request</span>
<span class="badge s-draft" id="mStatus">—</span>
<span style="font-family:var(--mono);font-size:12px;color:var(--teal)" id="mDocNum">#—</span>
</div>
<button class="modal-close" onclick="closeModal()">×</button>
</div>
<div class="modal-bd">
<div class="modal-tabs">
<div class="modal-tab on" onclick="mTab(this,'mt-info')">Details</div>
<div class="modal-tab" onclick="mTab(this,'mt-lines')">Lines</div>
</div>
<!-- Details pane -->
<div class="modal-tab-pane on" id="mt-info">
<div class="dt-grid">
<div>
<div class="dt-row"><span class="dt-l">Doc Entry</span><span class="dt-v" id="mDocEntry">—</span></div>
<div class="dt-row"><span class="dt-l">Doc No.</span><span class="dt-v" id="mDocNumD">—</span></div>
<div class="dt-row"><span class="dt-l">Posting Date</span><span class="dt-v" id="mDocDate">—</span></div>
<div class="dt-row"><span class="dt-l">Required By</span><span class="dt-v" id="mDueDate">—</span></div>
</div>
<div>
<div class="dt-row"><span class="dt-l">Vendor</span><span class="dt-v" id="mVendor">—</span></div>
<div class="dt-row"><span class="dt-l">Vendor Name</span><span class="dt-v" id="mVendorName">—</span></div>
<div class="dt-row"><span class="dt-l">Vendor Ref.</span><span class="dt-v" id="mRef">—</span></div>
<div class="dt-row"><span class="dt-l">Status</span><span class="dt-v" id="mStatusD">—</span></div>
</div>
</div>
<div class="dt-row" style="border-top:1px solid var(--border);margin-top:8px">
<span class="dt-l">Remarks</span>
<span class="dt-v" id="mComments" style="max-width:60%;word-break:break-word;text-align:right">—</span>
</div>
</div>
<!-- Lines pane -->
<div class="modal-tab-pane" id="mt-lines">
<table class="line-table">
<thead><tr>
<th>#</th><th>Item Code</th><th>Description</th>
<th>Required Date</th><th class="num">Qty</th><th>Unit</th><th class="num">Unit Price</th><th>Warehouse</th>
</tr></thead>
<tbody id="mLinesBody"></tbody>
</table>
</div>
</div>
<div class="modal-actions" id="mActions"></div>
</div>
</div>
<script>
'use strict';
const _tok = sessionStorage.getItem('portal_token');
if (!_tok) window.location.href = '/';
let _co = document.getElementById('coSel').value;
let _seq = 0;
let _whs = [];
let _curDoc = null;
let _srchTimer = null;
let _page = 0;
// Matches SAP Service Layer's own MaxPageSize (confirmed live: it silently
// caps a page at 20 rows regardless of a higher $top) — with PAGE_SIZE=30
// this was requesting 30, always getting 20 back, and "rows.length <
// PAGE_SIZE" (20 < 30) then wrongly read as "no more results", disabling
// Next even when more pages genuinely existed.
const PAGE_SIZE = 20;
const ah = () => ({ Authorization: `Bearer ${_tok}` });
const esc = s => String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
const dt = s => s ? s.slice(0,10) : '—';
// ── 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('toastC').appendChild(el);
setTimeout(() => el.remove(), 5000);
}
function spin(show, msg='Processing…'){
document.getElementById('fullSp').classList.toggle('show', show);
document.getElementById('spMsg').textContent = msg;
}
// ── Auth — read from sessionStorage (same as sidebar, no server round-trip) ──
let _user = null;
try {
const u = sessionStorage.getItem('portal_user');
if (u) {
_user = JSON.parse(u);
document.getElementById('userChip').textContent = _user?.name || _user?.username || '';
}
} catch(_e) {}
// Close/Cancel/Create Cancellation/Delete are destructive-ish SAP-document
// actions — restricted to admin/system_admin only; everyone else can view
// and create Purchase Requests but not act on an existing one this way.
const CAN_MANAGE_PR_DOC = _user && (_user.role === 'admin' || _user.role === 'system_admin');
function logout(){ sessionStorage.clear();document.cookie='portal_token=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';document.cookie='portal_user=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';; localStorage.removeItem('token');localStorage.removeItem('portal_user'); window.location.href='/'; }
function onCo(){ _co = document.getElementById('coSel').value; loadWarehouses(); loadDepartments(); }
async function populateCompanySelects(){
try{
const list=await window.fetchCompanyList();
if(!list.length)return;
document.querySelectorAll('#coSel').forEach(sel=>{
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){}
}
// ── Tab switching ──────────────────────────────────────────────
function switchTab(t){
['create','view'].forEach(id => {
document.getElementById('pane-'+id).style.display = id===t ? '' : 'none';
document.getElementById('tab-'+id).classList.toggle('on', id===t);
});
}
// ── Warehouses ─────────────────────────────────────────────────
async function loadWarehouses(){
try {
const r = await fetch(`/api/sap/lookup/warehouses?company=${_co}`, { headers: ah() });
const d = await r.json();
_whs = d.data || [];
document.querySelectorAll('.whs-sel').forEach(fillWhSel);
} catch {}
}
function fillWhSel(sel){
const cur = sel.value;
sel.innerHTML = '<option value="">— WH —</option>' +
_whs.map(w => `<option value="${esc(w.code)}"${w.code===cur?' selected':''}>${esc(w.code)} — ${esc(w.name)}</option>`).join('');
if (cur) sel.value = cur;
}
// ── Departments — live from SAP (OUDP), restricted per-user via Admin →
// Users → "PR Departments" (see routes/sap.js's /lookup/departments and
// /auth/profile's allowedDepartments; empty = no restriction, every
// department shown). Replaces what used to be a hardcoded, stale list.
async function loadDepartments(){
let allowed = [];
try {
const p = await fetch('/api/auth/profile', { headers: ah() });
const pd = await p.json();
if (Array.isArray(pd?.user?.allowedDepartments)) allowed = pd.user.allowedDepartments.map(String);
} catch {}
try {
const r = await fetch(`/api/sap/lookup/departments?company=${_co}`, { headers: ah() });
const d = await r.json();
let depts = d.data || [];
if (allowed.length) depts = depts.filter(dp => allowed.includes(dp.name));
const sel = document.getElementById('uDepart');
if (!sel) return;
const cur = sel.value;
sel.innerHTML = '<option value="">— Select Department —</option>' +
depts.map(dp => `<option value="${esc(dp.name)}"${dp.name===cur?' selected':''}>${esc(dp.name)}</option>`).join('');
if (cur) sel.value = cur;
} catch {}
}
// ── Vendor autocomplete ────────────────────────────────────────
let _vendTimer = null;
function vendorAc(val){
document.getElementById('cardCode').value = '';
clearTimeout(_vendTimer);
const dd = document.getElementById('vendorDd');
if (val.length < 2){ dd.classList.remove('open'); dd.innerHTML=''; return; }
_vendTimer = setTimeout(async () => {
try {
const r = await fetch(`/api/sap/lookup/vendors?q=${encodeURIComponent(val)}&company=${_co}`, { headers: ah() });
const d = await r.json();
const items = d.data || [];
dd.innerHTML = items.length
? items.map(v => `<div class="sdd-row" onclick="pickVendor('${esc(v.CardCode)}','${esc(v.CardName.replace(/'/g,"\'"))}')"><span class="sdd-code">${esc(v.CardCode)}</span><span class="sdd-name">${esc(v.CardName)}</span></div>`).join('')
: '<div class="sdd-msg">No vendors found</div>';
dd.classList.add('open');
} catch {}
}, 300);
}
function pickVendor(code, name){
document.getElementById('vendorSearch').value = `${code} — ${name}`;
document.getElementById('cardCode').value = code;
document.getElementById('vendorDd').classList.remove('open');
}
document.addEventListener('click', e => {
if (!document.getElementById('vendorWrap').contains(e.target))
document.getElementById('vendorDd').classList.remove('open');
});
// ── Item autocomplete per line ─────────────────────────────────
const _itemTimers = {};
function itemAc(inp, lineId){
const val = inp.value;
clearTimeout(_itemTimers[lineId]);
const row = document.getElementById('lr-'+lineId);
const dd = row?.querySelector('.item-dd');
if (!dd) return;
if (val.length < 2){ dd.classList.remove('open'); dd.innerHTML=''; return; }
_itemTimers[lineId] = setTimeout(async () => {
try {
const r = await fetch(`/api/sap/lookup/items?q=${encodeURIComponent(val)}&company=${_co}`, { headers: ah() });
const d = await r.json();
const items = d.data || [];
dd.innerHTML = items.length
? items.map(i => `<div class="sdd-row" onclick="pickItem(${lineId},'${esc(i.ItemCode)}','${esc((i.ItemName||'').replace(/'/g,"\'"))}','${esc(i.UoM||'')}','${esc(i.Warehouse||'')}')"><span class="sdd-code">${esc(i.ItemCode)}</span><span class="sdd-name">${esc(i.ItemName)}</span></div>`).join('')
: '<div class="sdd-msg">No items found</div>';
dd.classList.add('open');
const rect = inp.getBoundingClientRect();
dd.style.top = (rect.bottom + window.scrollY + 2) + 'px';
dd.style.left = rect.left + 'px';
dd.style.width = Math.max(rect.width, 260) + 'px';
} catch {}
}, 300);
}
function pickItem(lineId, code, name, uom, warehouse){
const row = document.getElementById('lr-'+lineId);
if (!row) return;
row.querySelector('.ic-inp').value = code;
row.querySelector('.desc-inp').value = name;
row.querySelector('.unit-inp').value = uom;
if (warehouse) {
const sel = row.querySelector('.whs-sel');
if (sel) sel.value = warehouse;
}
row.querySelector('.item-dd')?.classList.remove('open');
recalc();
}
// ── Line items ─────────────────────────────────────────────────
function addLine(){
const id = ++_seq;
const today = new Date().toISOString().slice(0,10);
const dueDate = document.getElementById('docDueDate').value || today;
const tr = document.createElement('tr');
tr.id = 'lr-'+id;
tr.innerHTML = `
<td><span class="li-num">${id}</span></td>
<td style="position:relative;min-width:120px">
<input class="ic-inp" type="text" placeholder="Item code…" autocomplete="off" oninput="itemAc(this,${id})" style="min-width:110px">
<div class="sdd item-dd" style="position:fixed;z-index:9999"></div>
</td>
<td><input class="desc-inp" type="text" placeholder="Description" readonly style="min-width:140px"></td>
<td><input class="rd-inp" type="date" value="${dueDate}" style="min-width:110px;font-size:10px;font-family:var(--mono)"></td>
<td class="nc"><input class="qty-inp" type="number" value="1" min="0" step="any" oninput="recalc()" style="width:65px"></td>
<td><input class="unit-inp" type="text" readonly style="width:45px;font-size:10px;color:var(--text3)"></td>
<td class="nc"><input class="price-inp" type="number" value="0" min="0" step="any" oninput="recalc()" style="width:90px"></td>
<td><select class="whs-sel fi" style="border:none;background:transparent;height:30px;padding:4px 6px;font-size:11px"></select></td>
<td><span class="xbtn" onclick="removeLine(${id})">×</span></td>`;
document.getElementById('linesBody').appendChild(tr);
// close dropdowns when clicking outside this row
document.addEventListener('click', function closeItemDd(e){
if (!tr.contains(e.target)){ tr.querySelector('.item-dd')?.classList.remove('open'); }
});
fillWhSel(tr.querySelector('.whs-sel'));
recalc();
}
function removeLine(id){ document.getElementById('lr-'+id)?.remove(); recalc(); }
function clearLines(){ document.getElementById('linesBody').innerHTML=''; _seq=0; recalc(); }
function recalc(){
let qty=0, amt=0;
document.querySelectorAll('#linesBody tr').forEach(row => {
qty += parseFloat(row.querySelector('.qty-inp')?.value||0);
amt += parseFloat(row.querySelector('.qty-inp')?.value||0) * parseFloat(row.querySelector('.price-inp')?.value||0);
});
document.getElementById('totQty').textContent = qty;
document.getElementById('totAmt').textContent = amt.toFixed(2);
}
// ── Reset ──────────────────────────────────────────────────────
function resetForm(){
const today = new Date().toISOString().slice(0,10);
document.getElementById('docDate').value = today;
document.getElementById('docDueDate').value = today;
document.getElementById('numAtCard').value = '';
document.getElementById('vendorSearch').value = '';
document.getElementById('cardCode').value = '';
document.getElementById('comments').value = '';
document.getElementById('uDepart').value = '';
document.getElementById('uStsReqd').value = 'No';
clearLines();
}
// ── Submit PR — validate then show approval dialog ─────────────
let _pendingPayload = null;
function submitPR(){
const lines = [];
let valid = true;
document.querySelectorAll('#linesBody tr').forEach(row => {
const ic = row.querySelector('.ic-inp')?.value.trim();
const qty = parseFloat(row.querySelector('.qty-inp')?.value || 0);
if (!ic || qty <= 0){ valid=false; return; }
const line = {
ItemCode: ic,
ItemDescription: row.querySelector('.desc-inp')?.value || '',
Quantity: qty,
UnitPrice: parseFloat(row.querySelector('.price-inp')?.value||0),
};
const rd = row.querySelector('.rd-inp')?.value;
const whs = row.querySelector('.whs-sel')?.value;
if (rd) line.RequiredDate = rd;
if (whs) line.WarehouseCode = whs;
lines.push(line);
});
if (!valid){ toast('Each line needs an item code and qty > 0','err'); return; }
if (!lines.length){ toast('Add at least one line item','err'); return; }
const docDate = document.getElementById('docDate').value;
const docDueDate = document.getElementById('docDueDate').value;
const cardCode = document.getElementById('cardCode').value.trim();
const numAtCard = document.getElementById('numAtCard').value.trim();
const comments = document.getElementById('comments').value.trim();
const depart = document.getElementById('uDepart').value;
const stsReqd = document.getElementById('uStsReqd').value;
if (!comments){ toast('Remarks / Purpose is required','err'); return; }
if (!depart){ toast('Select a Department','err'); return; }
_pendingPayload = { company: _co, DocumentLines: lines };
if (docDate) _pendingPayload.DocDate = docDate;
if (docDueDate) { _pendingPayload.DocDueDate = docDueDate; _pendingPayload.RequriedDate = docDueDate; }
if (cardCode) _pendingPayload.CardCode = cardCode;
if (numAtCard) _pendingPayload.NumAtCard = numAtCard;
if (comments) _pendingPayload.Comments = comments;
if (docDueDate) _pendingPayload.U_ReqDqte = docDueDate;
if (depart) { _pendingPayload.U_Depart = depart; _pendingPayload.U_Department = depart; }
_pendingPayload.U_STS_REQD = stsReqd;
// Show approval remarks dialog (like SAP desktop "Request for Document Generation")
document.getElementById('approvalRemarks').value = '';
document.getElementById('approvalDialog').classList.add('show');
setTimeout(() => document.getElementById('approvalRemarks').focus(), 100);
}
function closeApprovalDialog(){
document.getElementById('approvalDialog').classList.remove('show');
_pendingPayload = null;
}
async function confirmApproval(){
if (!_pendingPayload) return;
const remarks = document.getElementById('approvalRemarks').value.trim();
if (remarks) {
_pendingPayload.Comments = (_pendingPayload.Comments ? _pendingPayload.Comments + '\n' : '') + 'Approval Remarks: ' + remarks;
}
document.getElementById('btnApprovalOk').disabled = true;
document.getElementById('approvalDialog').classList.remove('show');
document.getElementById('btnPost').disabled = true;
document.getElementById('spPost').style.display = 'inline-block';
try {
const r = await fetch('/api/sap/purchase-request', {
method:'POST', headers:{...ah(),'Content-Type':'application/json'},
body: JSON.stringify(_pendingPayload),
});
const d = await r.json();
if (!r.ok || !d.success) throw new Error(d.message||'SAP error');
toast(`Purchase Request #${d.data?.DocEntry || ''} submitted — pending approval`, 'ok');
resetForm();
_pendingPayload = null;
} catch(err){ toast(err.message,'err'); }
finally {
document.getElementById('btnPost').disabled = false;
document.getElementById('spPost').style.display = 'none';
document.getElementById('btnApprovalOk').disabled = false;
}
}
// ── List / Search ──────────────────────────────────────────────
function debounceSearch(){
clearTimeout(_srchTimer);
_srchTimer = setTimeout(() => { _page=0; loadList(); }, 400);
}
async function loadList(){
const wrap = document.getElementById('listWrap');
wrap.innerHTML = '<div class="pr-empty">Loading…</div>';
const q = document.getElementById('srchQ').value.trim();
const status = document.getElementById('srchStatus').value;
const skip = _page * PAGE_SIZE;
let url = `/api/sap/purchase-requests?company=${_co}&$top=${PAGE_SIZE}&$skip=${skip}`;
if (q) url += `&q=${encodeURIComponent(q)}`;
if (status) url += `&status=${encodeURIComponent(status)}`;
try {
const r = await fetch(url, { headers: ah() });
const d = await r.json();
const rows = d.data || [];
if (!rows.length){ wrap.innerHTML='<div class="pr-empty">No purchase requests found.</div>'; document.getElementById('pgBar').style.display='none'; return; }
wrap.innerHTML = `<div class="pr-list">${rows.map(pr => `
<div class="pr-card" onclick="openDoc(${pr.DocEntry})">
<div class="pr-card-top">
<span class="pr-docnum">#${esc(String(pr.DocNum||pr.DocEntry))}</span>
${statusBadge(pr.DocumentStatus)}
</div>
<div class="pr-icon">📝</div>
${pr.Comments ? `<div class="pr-comment" title="${esc(pr.Comments)}">${esc(pr.Comments)}</div>` : ''}
<div class="pr-meta">
<span>Date <b>${dt(pr.DocDate)}</b></span>
<span>Required <b>${dt(pr.DocDueDate)}</b></span>
${pr.CardCode ? `<span>Vendor <b>${esc(pr.CardCode)}</b></span>` : ''}
</div>
</div>`).join('')}</div>`;
// Pagination
const pgBar = document.getElementById('pgBar');
pgBar.style.display = 'flex';
document.getElementById('pgPrev').disabled = _page === 0;
document.getElementById('pgNext').disabled = rows.length < PAGE_SIZE;
document.getElementById('pgInfo').textContent = `Page ${_page+1}`;
} catch(err){ wrap.innerHTML=`<div class="pr-empty" style="color:var(--red)">${esc(err.message)}</div>`; }
}
function pgMove(dir){ _page = Math.max(0, _page+dir); loadList(); }
function statusBadge(s){
if (s==='bost_Open') return `<span class="badge s-open">Open</span>`;
if (s==='bost_Close') return `<span class="badge s-closed">Closed</span>`;
if (s==='bost_Cancelled') return `<span class="badge s-cancelled">Cancelled</span>`;
return `<span class="badge s-draft">${esc(s||'Draft')}</span>`;
}
// ── Document detail ────────────────────────────────────────────
async function openDoc(id){
spin(true,'Loading document…');
try {
const r = await fetch(`/api/sap/purchase-requests/${id}?company=${_co}`, { headers: ah() });
const d = await r.json();
if (!r.ok || !d.success) throw new Error(d.message||'Not found');
_curDoc = d.data;
renderModal(_curDoc);
document.getElementById('docModal').classList.add('show');
} catch(err){ toast(err.message,'err'); }
finally { spin(false); }
}
function renderModal(doc){
const s = doc.DocumentStatus;
document.getElementById('mStatus').className = 'badge ' + (s==='bost_Open'?'s-open':s==='bost_Close'?'s-closed':'s-draft');
document.getElementById('mStatus').textContent = s==='bost_Open'?'Open':s==='bost_Close'?'Closed':'Draft';
document.getElementById('mDocNum').textContent = '#' + (doc.DocNum||doc.DocEntry);
document.getElementById('mDocEntry').textContent = doc.DocEntry;
document.getElementById('mDocNumD').textContent = doc.DocNum;
document.getElementById('mDocDate').textContent = dt(doc.DocDate);
document.getElementById('mDueDate').textContent = dt(doc.DocDueDate);
document.getElementById('mVendor').textContent = doc.CardCode || '—';
document.getElementById('mVendorName').textContent= doc.CardName || '—';
document.getElementById('mRef').textContent = doc.NumAtCard || '—';
document.getElementById('mStatusD').textContent = s || '—';
document.getElementById('mComments').textContent = doc.Comments || '—';
// Lines
const lines = doc.DocumentLines || [];
document.getElementById('mLinesBody').innerHTML = lines.length
? lines.map((l,i) => `<tr>
<td>${i+1}</td>
<td style="font-family:var(--mono);color:var(--teal)">${esc(l.ItemCode||'—')}</td>
<td>${esc(l.ItemDescription||'—')}</td>
<td style="font-family:var(--mono);font-size:10px">${dt(l.RequiredDate)}</td>
<td class="num">${l.Quantity||0}</td>
<td>${esc(l.UoMCode||l.unitOfMeasure||'—')}</td>
<td class="num">${parseFloat(l.UnitPrice||0).toFixed(2)}</td>
<td>${esc(l.WarehouseCode||'—')}</td>
</tr>`).join('')
: '<tr><td colspan="8" style="text-align:center;padding:16px;color:var(--text3)">No lines</td></tr>';
// Actions
const isOpen = s === 'bost_Open';
const isClosed = s === 'bost_Close';
const actions = document.getElementById('mActions');
actions.innerHTML = `
${isOpen && CAN_MANAGE_PR_DOC ? `<button class="btn b-green btn-sm" onclick="docAction('close')">✓ Close</button>` : ''}
${isOpen && CAN_MANAGE_PR_DOC ? `<button class="btn b-warn btn-sm" onclick="docAction('cancel')">✕ Cancel</button>` : ''}
${isClosed ? `<button class="btn b-blue btn-sm" onclick="docAction('reopen')">↩ Reopen</button>` : ''}
${isOpen && CAN_MANAGE_PR_DOC ? `<button class="btn b-secondary btn-sm" onclick="docAction('create-cancellation')">Create Cancellation</button>` : ''}
${isOpen && CAN_MANAGE_PR_DOC ? `<button class="btn b-danger btn-sm" onclick="docAction('delete')">Delete</button>` : ''}
`;
}
function mTab(el, paneId){
document.querySelectorAll('.modal-tab').forEach(t => t.classList.remove('on'));
document.querySelectorAll('.modal-tab-pane').forEach(p => p.classList.remove('on'));
el.classList.add('on');
document.getElementById(paneId).classList.add('on');
}
function closeModal(){ document.getElementById('docModal').classList.remove('show'); _curDoc=null; }
document.getElementById('docModal').addEventListener('click', e => {
if (e.target === document.getElementById('docModal')) closeModal();
});
// ── Document actions ───────────────────────────────────────────
async function docAction(action){
if (!_curDoc) return;
const id = _curDoc.DocEntry;
const labels = { close:'Close', cancel:'Cancel', reopen:'Reopen', delete:'Delete', 'create-cancellation':'Create Cancellation' };
const label = labels[action] || action;
if (['delete','cancel'].includes(action)){
if (!confirm(`Are you sure you want to ${label} Purchase Request #${_curDoc.DocNum}?`)) return;
}
spin(true, `${label}ing…`);
try {
let r, d;
if (action === 'delete'){
r = await fetch(`/api/sap/purchase-requests/${id}?company=${_co}`, { method:'DELETE', headers: ah() });
d = await r.json();
} else {
r = await fetch(`/api/sap/purchase-requests/${id}/${action}?company=${_co}`, { method:'POST', headers: ah() });
d = await r.json();
}
if (!r.ok || !d.success) throw new Error(d.message||'Action failed');
toast(`Purchase Request ${label}d successfully`, 'ok');
closeModal();
loadList();
} catch(err){ toast(err.message,'err'); }
finally { spin(false); }
}
// ── Init ───────────────────────────────────────────────────────
(async () => {
await populateCompanySelects();
_co = document.getElementById('coSel').value;
const today = new Date().toISOString().slice(0,10);
document.getElementById('docDate').value = today;
document.getElementById('docDueDate').value = today;
await loadWarehouses();
await loadDepartments();
addLine();
})();
</script>
<script src="/sidebar.js?v=25"></script>
</body>
</html>