Files
sap-erp/public/purchase-request.html
John eead8f5ffd
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s
sale order
2026-10-05 18:45:17 +05:30

994 lines
52 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>
<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>
<option value="drafts">Pending SAP Approval</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;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
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 || [];
// Built as real DOM elements with a bound click handler, not an
// inline onclick="..." string — a vendor name containing a quote
// character (e.g. an apostrophe) would otherwise terminate that
// string/attribute early and silently break the click entirely, same
// bug class as pickItem() below.
dd.innerHTML = '';
if (items.length) {
items.forEach(v => {
const row = document.createElement('div');
row.className = 'sdd-row';
row.innerHTML = `<span class="sdd-code">${esc(v.CardCode)}</span><span class="sdd-name">${esc(v.CardName)}</span>`;
row.onclick = () => pickVendor(v.CardCode, v.CardName);
dd.appendChild(row);
});
} else {
dd.innerHTML = '<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}&activeOnly=1`, { headers: ah() });
const d = await r.json();
const items = d.data || [];
// Built as real DOM elements with a bound click handler, not an
// inline onclick="..." string — an item description containing a
// quote character (e.g. an inch mark, "NYLON RUBBER BAND 1"") would
// otherwise terminate that HTML attribute early, silently corrupting
// the click handler so picking the item did nothing at all.
dd.innerHTML = '';
if (items.length) {
items.forEach(i => {
const row = document.createElement('div');
row.className = 'sdd-row';
row.innerHTML = `<span class="sdd-code">${esc(i.ItemCode)}</span><span class="sdd-name">${esc(i.ItemName)}</span>`;
row.onclick = () => pickItem(lineId, i.ItemCode, i.ItemName || '', i.UoM || '', i.Warehouse || '');
dd.appendChild(row);
});
} else {
dd.innerHTML = '<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}" oninput="this.dataset.manual='1'" 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();
}
// Header "Required By" flows down to every line date the user hasn't set by hand
function syncLineDates(){
const due = document.getElementById('docDueDate').value;
if (!due) return;
document.querySelectorAll('#linesBody .rd-inp').forEach(inp => {
if (!inp.dataset.manual) inp.value = due;
});
}
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. If it needed SAP approval it won't show under Open/Closed yet — check "Pending SAP Approval" in the status filter.`, '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);
}
// A Purchase Request that triggers one of SAP's own Approval Procedures
// isn't created as a real PurchaseRequests document at all — DI API creates
// it as a SAP Draft instead, sitting there until an SAP approver acts on it
// (see "SAP Approval" for that). It has no DocumentStatus (Open/Closed), so
// it never showed up here at all — the requester submitted it and then had
// no way to even confirm it existed. "Pending SAP Approval" queries the
// dedicated GET /purchase-request/drafts endpoint instead of the normal
// PurchaseRequests list.
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;
if (status === 'drafts'){ await loadDraftList(q); return; }
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 || [];
const total = typeof d.total === 'number' ? d.total : null;
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 = total!=null ? (_page+1)*PAGE_SIZE >= total : rows.length < PAGE_SIZE;
document.getElementById('pgInfo').textContent = total!=null
? `Page ${_page+1} of ${Math.max(1,Math.ceil(total/PAGE_SIZE))} · ${total} request${total===1?'':'s'}`
: `Page ${_page+1}`;
} catch(err){ wrap.innerHTML=`<div class="pr-empty" style="color:var(--red)">${esc(err.message)}</div>`; }
}
async function loadDraftList(q){
const wrap = document.getElementById('listWrap');
document.getElementById('pgBar').style.display = 'none';
try {
const r = await fetch(`/api/sap/purchase-request/drafts?company=${_co}`, { headers: ah() });
const d = await r.json();
let rows = d.data || [];
if (q){
const needle = q.toUpperCase();
rows = rows.filter(pr => [pr.CardCode, pr.CardName, pr.Comments].some(v => (v||'').toUpperCase().includes(needle)));
}
if (!rows.length){ wrap.innerHTML = '<div class="pr-empty">No purchase requests pending SAP approval.</div>'; return; }
wrap.innerHTML = `<div class="pr-list">${rows.map(pr => `
<div class="pr-card" style="cursor:default">
<div class="pr-card-top">
<span class="pr-docnum">Draft #${esc(String(pr.DocEntry))}</span>
<span class="badge s-draft">⏳ Pending SAP Approval</span>
</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.RequriedDate||pr.DocDueDate)}</b></span>
${pr.CardCode ? `<span>Vendor <b>${esc(pr.CardCode)}</b></span>` : ''}
${(pr.U_Depart||pr.U_Department) ? `<span>Dept <b>${esc(pr.U_Depart||pr.U_Department)}</b></span>` : ''}
</div>
</div>`).join('')}</div>`;
} 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;
document.getElementById('docDueDate').addEventListener('change', syncLineDates);
await loadWarehouses();
await loadDepartments();
addLine();
})();
</script>
<script src="/sidebar.js?v=31"></script>
</body>
</html>