Files
sap-erp/dist-1/public/purchase-quotation.html
John 69b4e68baf
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s
first commit
2026-09-23 17:31:02 +05:30

1138 lines
60 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 Quotation — 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{
--teal:#047e7e;--teal2:#035b5b;--teal3:#06b6b6;
--blue:#1d4ed8;--amber:#d97706;--green:#059669;--red:#dc2626;
--text:#111827;--text2:#374151;--text3:#6b7280;--text4:#9ca3af;
--bg:#f0f2f5;--surface:#fff;--border:#e5e7eb;--border2:#d1d5db;
--surface2:#f9fafb;--surface3:#f3f4f6;
--radius:10px;--font:'Space Grotesk',sans-serif;
--mono:'JetBrains Mono',monospace;
--shadow:0 4px 20px rgba(0,0,0,.1)
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:var(--font);background:var(--bg);color:var(--text);min-height:100vh;-webkit-font-smoothing:antialiased}
/* nav is removed by sidebar.js — keep hidden */
nav{display:none}
main{padding:14px 20px 80px}
/* ── Tabs ── */
.tabs{display:flex;gap:4px;margin-bottom:14px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:4px;box-shadow:0 1px 3px rgba(0,0,0,.04)}
.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);background:var(--surface3)}
.tab.on{background:linear-gradient(135deg,var(--teal),var(--teal2));color:#fff;box-shadow:0 2px 8px rgba(4,126,126,.3)}
/* ── Cards ── */
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:12px;box-shadow:0 1px 3px rgba(0,0,0,.05)}
.card-hd{background:var(--surface3);padding:10px 16px;font-size:11px;font-weight:800;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:500;color:var(--text3)}
.card-bd{padding:16px}
/* ── 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:8px 11px;border:1.5px solid var(--border);border-radius:8px;font-size:13px;font-family:var(--font);background:var(--surface);color:var(--text);outline:none;height:36px;transition:border-color .15s,box-shadow .15s}
.fi:focus{border-color:var(--teal);box-shadow:0 0 0 3px rgba(4,126,126,.1)}
.fi::placeholder{color:var(--text4)}
.fi[readonly]{background:var(--surface2);color:var(--text3);cursor:default}
select.fi{cursor:pointer;appearance:none;background:var(--surface) 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='%236b7280'/%3E%3C/svg%3E") no-repeat right 10px center;padding-right:26px}
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}}
/* ── Autocomplete ── */
.sw{position:relative}
.sdd{position:absolute;top:100%;left:0;right:0;background:var(--surface);border:1.5px solid var(--teal);border-radius:8px;box-shadow:0 8px 30px rgba(0,0,0,.12);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,.sdd-row.hi{background:#ecfeff}
.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}
.acd-fixed{position:fixed;min-width:240px;background:var(--surface);border:1.5px solid var(--teal);border-radius:8px;box-shadow:0 8px 30px rgba(0,0,0,.12);z-index:99999;max-height:240px;overflow-y:auto;display:none}
.acd-fixed.open{display:block}
.aci{padding:8px 12px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;gap:8px;border-bottom:1px solid var(--border);transition:background .08s}
.aci:last-child{border-bottom:none}
.aci:hover,.aci.hi{background:#ecfeff}
.ac-name{font-size:11px;color:var(--text2);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ac-code{font-family:var(--mono);font-size:10px;color:var(--teal);flex-shrink:0}
.ac-msg{padding:10px 12px;color:var(--text3);font-size:11px;font-style:italic}
.spin{display:inline-block;width:12px;height:12px;border:2px solid rgba(4,126,126,.2);border-top-color:var(--teal);border-radius:50%;animation:spin .6s linear infinite;flex-shrink:0}
/* ── Lines 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:#f0fdf4}
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:#ecfeff;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;line-height:1;transition:color .12s}.xbtn:hover{color:var(--red)}
.tfoot-row{background:var(--surface3);border-top:2px solid var(--border)}
.tfoot-row td{padding:7px 10px;font-size:11px;font-weight:700;color:var(--text2);font-family:var(--mono)}
.tfoot-row .tl{font-size:10px;color:var(--text3);font-weight:600;text-transform:uppercase;letter-spacing:.4px}
.tfoot-row .tv{color:var(--teal)}
/* ── 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:linear-gradient(135deg,var(--teal),var(--teal2));color:#fff}.b-primary:hover:not(:disabled){box-shadow:0 4px 14px rgba(4,126,126,.4);transform:translateY(-1px)}
.b-secondary{background:var(--surface);color:var(--text2);border:1.5px solid var(--border)}.b-secondary:hover{border-color:var(--teal);color:var(--teal)}
.b-danger{background:#fef2f2;color:var(--red);border:1px solid #fecaca}.b-danger:hover{background:var(--red);color:#fff}
.b-warn{background:#fffbeb;color:var(--amber);border:1px solid #fde68a}.b-warn:hover{background:#fef3c7}
.b-green{background:#f0fdf4;color:var(--green);border:1px solid #a7f3d0}.b-green:hover{background:#d1fae5}
.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:#d1fae5;color:#065f46;border:1px solid #a7f3d0}
.s-closed{background:#f3f4f6;color:var(--text3);border:1px solid var(--border2)}
.s-draft{background:#fffbeb;color:#92400e;border:1px solid #fde68a}
/* ── PQ list cards ── */
.pq-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:14px}
.pq-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px 14px 14px 18px;cursor:pointer;transition:all .18s;position:relative;overflow:hidden;aspect-ratio:1/1;display:flex;flex-direction:column}
.pq-card::before{content:'';position:absolute;left:0;top:0;bottom:0;width:4px;background:var(--teal)}
.pq-card:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,.10);border-color:var(--teal3)}
.pq-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;flex-wrap:wrap}
.pq-docnum{font-family:var(--mono);font-size:12.5px;color:var(--teal);font-weight:700}
.pq-vendor{font-size:12.5px;color:var(--text);font-weight:600;margin:8px 0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.pq-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)}
.pq-meta b{color:var(--text2);font-weight:600}
.pq-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;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px;pointer-events:none}
.toast{padding:10px 16px;border-radius:10px;font-size:12px;font-weight:600;box-shadow:0 4px 20px rgba(0,0,0,.15);animation:sIn .22s ease;max-width:320px;pointer-events:auto;display:flex;align-items:center;gap:8px}
@keyframes sIn{from{transform:translateY(12px);opacity:0}to{transform:translateY(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,.4);z-index:9000;align-items:center;justify-content:center;flex-direction:column;gap:12px;backdrop-filter:blur(3px)}
.full-sp.show{display:flex}
.full-sp .sp{width:36px;height:36px;border-width:3px}
.full-sp p{font-size:12px;color:#fff;font-family:var(--mono)}
/* ── Modal ── */
.modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:500;display:none;align-items:flex-start;justify-content:center;padding:20px;overflow-y:auto;backdrop-filter:blur(3px)}
.modal-bg.show{display:flex}
.modal{background:var(--surface);border:1px solid var(--border);border-radius:16px;width:100%;max-width:800px;margin:auto;box-shadow:0 24px 80px rgba(0,0,0,.15);animation:mIn .2s ease}
@keyframes mIn{from{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}
.modal-hd{padding:14px 20px;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:7px;border:1.5px solid var(--border);background:var(--surface3);color:var(--text3);cursor:pointer;font-size:16px;line-height:1;display:flex;align-items:center;justify-content:center;transition:all .15s}.modal-close:hover{background:#fee2e2;color:var(--red);border-color:#fca5a5}
.modal-bd{padding:16px 20px;max-height:72vh;overflow-y:auto}
.modal-tabs{display:flex;gap:3px;margin-bottom:12px;border-bottom:2px solid var(--border)}
.modal-tab{padding:7px 14px;font-size:11px;font-weight:700;color:var(--text3);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-2px;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;border:1px solid var(--border);border-radius:8px;overflow:hidden}
.dt-row{display:flex;flex-direction:column;padding:8px 12px;border-bottom:1px solid var(--border);gap:2px}
.dt-row:nth-last-child(-n+2){border-bottom:none}
.dt-row.full{grid-column:1/-1}
.dt-l{color:var(--text3);font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.4px}
.dt-v{color:var(--text);font-size:13px;font-weight:600}
.modal-actions{display:flex;gap:8px;flex-wrap:wrap;padding:12px 20px;border-top:1px solid var(--border);background:var(--surface3);border-radius:0 0 16px 16px}
/* ── Lines table in modal ── */
.line-table{width:100%;border-collapse:collapse;font-size:11px;margin-top:4px;border:1px solid var(--border);border-radius:8px;overflow:hidden}
.line-table th{background:var(--surface3);padding:7px 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 #f3f4f6;color:var(--text2);vertical-align:top}
.line-table tbody tr:last-child td{border-bottom:none}
.line-table tr:hover td{background:#f9fafb}
.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:5px 12px;border-radius:7px;border:1.5px solid var(--border);background:var(--surface);color:var(--text2);font-size:11px;cursor:pointer;font-family:var(--font);font-weight:600;transition:all .15s}.pg-bar button:hover:not(:disabled){border-color:var(--teal);color:var(--teal)}.pg-bar button:disabled{opacity:.35}
/* ── co-sel (nav) ── */
.co-sel{background:var(--surface2);border:1.5px solid var(--border);color:var(--text);border-radius:7px;padding:5px 10px;font-size:12px;font-family:var(--font);cursor:pointer;outline:none}
.user-chip{font-size:12px;color:var(--text3);font-weight:600}
.btn-logout{padding:5px 12px;border-radius:7px;font-size:11px;font-weight:700;border:1.5px solid var(--border);color:var(--text3);background:none;cursor:pointer;font-family:var(--font)}.btn-logout:hover{border-color:var(--red);color:var(--red)}
</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">PQ</span>
<div style="min-width:0">
<div class="nav-title">Purchase Quotation</div>
<div class="nav-sub">Purchase Quotations · SAP B1</div>
</div>
</div>
<div class="nav-right">
<span class="user-chip" id="userChip"></span>
<select class="co-sel" id="coSel">
</select>
<button class="btn-logout" onclick="logout()">Logout</button>
</div>
</nav>
<main>
<div class="tabs" style="justify-content:space-between;align-items:center">
<div style="display:flex;gap:4px">
<button class="tab on" id="tab-create" onclick="switchTab('create')">New Quotation</button>
<button class="tab" id="tab-view" onclick="switchTab('view');loadQuotations()">View / Search</button>
</div>
<select class="fi" id="coSel2" 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 Card -->
<div class="card">
<div class="card-hd"><span class="ico"></span>Purchase Quotation Header
<span class="right">Vendor required · Validity auto-set to +30 days</span>
</div>
<div class="card-bd">
<div class="fi-row4" style="margin-bottom:10px">
<div class="fg" style="grid-column:span 2">
<span class="fl">Vendor <span class="req">*</span></span>
<div class="sw" id="vendorWrap">
<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>
<div class="fg">
<span class="fl">Document Type <span class="req">*</span></span>
<select class="fi" id="docType" onchange="onDocTypeChange()">
<option value="dDocument_Items">Item</option>
<option value="dDocument_Service">Service</option>
</select>
</div>
<div class="fg">
<span class="fl">Vendor Ref No.</span>
<input class="fi" type="text" id="vendorRef" placeholder="e.g. VR-2024-001">
</div>
</div>
<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">Validity Date</span>
<input class="fi" type="date" id="docDueDate">
</div>
<div class="fg">
<span class="fl">Required Date</span>
<input class="fi" type="date" id="reqDate">
</div>
<div class="fg">
<span class="fl">Doc No. / Status</span>
<input class="fi" type="text" id="docNumDisp" value="AUTO · Draft" readonly>
</div>
</div>
<div class="fg">
<span class="fl">Remarks</span>
<textarea class="fi" id="comments" rows="2" placeholder="Optional remarks for this quotation…"></textarea>
</div>
</div>
</div>
<!-- Lines Card -->
<div class="card">
<div class="card-hd"><span class="ico"></span>Document Lines
<span id="docTypeBadge" style="font-size:10px;padding:2px 8px;border-radius:5px;background:rgba(4,126,126,.15);color:var(--teal);border:1px solid rgba(4,126,126,.3);margin-left:4px">Item</span>
<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" id="linesWrap">
<table id="linesTable">
<thead>
<tr id="thead-row">
<th style="width:30px">#</th>
<th class="col-itemcode" style="min-width:110px">Item Code <span class="req">*</span></th>
<th style="min-width:150px">Description <span class="req" id="desc-req" style="display:none">*</span></th>
<th style="width:70px;text-align:right">Qty <span class="req">*</span></th>
<th style="width:100px;text-align:right">Unit Price <span class="req">*</span></th>
<th style="width:60px;text-align:right">Disc %</th>
<th style="min-width:120px">Tax Code</th>
<th style="width:55px;text-align:right">Tax %</th>
<th style="width:100px;text-align:right">Total</th>
<th class="col-warehouse" style="min-width:90px">Warehouse</th>
<th style="width:26px"></th>
</tr>
</thead>
<tbody id="tbody"></tbody>
<tfoot>
<tr class="tfoot-row">
<td colspan="7" style="padding:8px 10px"><span class="tl">TOTALS</span></td>
<td colspan="2" class="nc" style="padding:8px 10px">
<div style="display:flex;flex-direction:column;gap:2px;align-items:flex-end">
<span style="font-size:9px;color:var(--text3)">Subtotal: <span class="tv" id="totSub">0.00</span></span>
<span style="font-size:9px;color:var(--text3)">Discount: <span class="tv" id="totDisc">0.00</span></span>
<span style="font-size:9px;color:var(--text3)">Tax: <span class="tv" id="totTax">0.00</span></span>
<span style="font-size:11px;font-weight:700;color:var(--text)">Total: <span style="color:var(--teal)" id="totDoc">0.00</span></span>
</div>
</td>
<td colspan="2"></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="btn-bar">
<button class="btn b-primary" id="postBtn" onclick="submitPQ()">
<span class="sp" id="spPost" style="display:none"></span> ✓ Post PQ
</button>
<button class="btn b-secondary" onclick="openCopyFrom()">Copy From (Indent)</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 Quotations</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="loadQuotations()">
<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="loadQuotations()">Search</button>
</div>
</div>
</div>
<div id="listWrap">
<div class="pq-empty">Click "View / Search" to load purchase quotations.</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>
<!-- ══ COPY FROM MODAL (PR Chooser) ══ -->
<div class="modal-bg" id="copyFromModal">
<div class="modal" style="max-width:900px">
<div class="modal-hd">
<div class="modal-hd-left">
<span style="font-size:18px"></span>
<span style="font-weight:700;font-size:13px">List of Purchase Requests (Indents)</span>
</div>
<button class="modal-close" onclick="closeCopyFrom()">×</button>
</div>
<div class="modal-bd" style="padding:12px 16px">
<div style="display:flex;gap:8px;margin-bottom:10px">
<input class="fi" type="text" id="cfSearch" placeholder="Search by doc no. or remarks…" oninput="cfDebounce()" style="flex:1">
<button class="btn b-secondary btn-sm" onclick="loadPRList()">Search</button>
</div>
<div style="border:1px solid var(--border);border-radius:8px;overflow:hidden;max-height:420px;overflow-y:auto">
<table style="width:100%;border-collapse:collapse;font-size:11px">
<thead style="position:sticky;top:0;z-index:5">
<tr style="background:var(--surface3)">
<th style="padding:7px 8px;text-align:left;font-size:9px;font-weight:700;color:var(--text3);text-transform:uppercase;border-bottom:1px solid var(--border);width:32px">#</th>
<th style="padding:7px 8px;text-align:left;font-size:9px;font-weight:700;color:var(--text3);text-transform:uppercase;border-bottom:1px solid var(--border);width:70px">PRQ No.</th>
<th style="padding:7px 8px;text-align:left;font-size:9px;font-weight:700;color:var(--text3);text-transform:uppercase;border-bottom:1px solid var(--border);width:90px">Posting Date</th>
<th style="padding:7px 8px;text-align:left;font-size:9px;font-weight:700;color:var(--text3);text-transform:uppercase;border-bottom:1px solid var(--border);width:90px">Required Date</th>
<th style="padding:7px 8px;text-align:left;font-size:9px;font-weight:700;color:var(--text3);text-transform:uppercase;border-bottom:1px solid var(--border);width:120px">Vendor</th>
<th style="padding:7px 8px;text-align:left;font-size:9px;font-weight:700;color:var(--text3);text-transform:uppercase;border-bottom:1px solid var(--border)">Remarks</th>
</tr>
</thead>
<tbody id="cfBody">
<tr><td colspan="6" style="padding:24px;text-align:center;color:var(--text3)">Loading…</td></tr>
</tbody>
</table>
</div>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end;padding:10px 16px;border-top:1px solid var(--border);background:var(--surface3);border-radius:0 0 12px 12px">
<button class="btn b-secondary" onclick="closeCopyFrom()">Cancel</button>
<button class="btn b-primary" id="btnChoose" onclick="choosePR()" disabled>✓ Choose</button>
</div>
</div>
</div>
<!-- Fixed AC portal (outside scroll context) -->
<div class="acd-fixed" id="acPortal"></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 Quotation</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>
<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">Validity Date</span><span class="dt-v" id="mDueDate">—</span></div>
</div>
<div>
<div class="dt-row"><span class="dt-l">Vendor Code</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">Total</span><span class="dt-v" id="mTotal" style="color:var(--teal)">—</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>
<div class="modal-tab-pane" id="mt-lines">
<table class="line-table">
<thead><tr>
<th>#</th><th>Item Code</th><th>Description</th>
<th class="num">Qty</th><th class="num">Unit Price</th>
<th class="num">Disc %</th><th>Tax Code</th><th class="num">Total</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 lineSeq = 0;
let _taxCodes= [];
let _whs = [];
let _portalTarget = null;
let _curDoc = null;
let _srchTimer = null;
let _page = 0;
const PAGE_SIZE = 30;
const ah = () => ({ Authorization: `Bearer ${_tok}` });
const esc = s => String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
const dt = s => s ? s.slice(0,10) : '—';
const fmt = n => parseFloat(n||0).toFixed(2);
// ── Toast ──────────────────────────────────────────────
function toast(msg, type='info'){
const el = document.createElement('div');
el.className = `toast t-${type}`;
el.innerHTML = `<span>${{ok:'✓',err:'✕',info:'ℹ'}[type]||'ℹ'}</span><span>${esc(String(msg))}</span>`;
document.getElementById('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 / user chip ───────────────────────────────────
try {
const u = sessionStorage.getItem('portal_user');
if (u){ const user=JSON.parse(u); document.getElementById('userChip').textContent=user?.name||user?.username||''; }
} catch{}
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='/'; }
async function populateCompanySelects(){
try{
const list=await window.fetchCompanyList();
if(!list.length)return;
['coSel','coSel2'].forEach(id=>{
const sel=document.getElementById(id);if(!sel)return;
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){}
}
function onCo(src){
const v = src === 'nav'
? document.getElementById('coSel').value
: document.getElementById('coSel2').value;
document.getElementById('coSel').value = v;
document.getElementById('coSel2').value = v;
_co = v;
_taxCodes=[]; _whs=[];
loadTaxCodes(); loadWarehouses();
// refresh view list if it is currently visible
if (document.getElementById('pane-view').style.display !== 'none'){ _page=0; loadQuotations(); }
// refresh Copy From PR list if modal is open
if (document.getElementById('copyFromModal').classList.contains('show')) loadPRList();
}
function isService(){ return document.getElementById('docType').value === 'dDocument_Service'; }
function onDocTypeChange(){
const svc = isService();
document.getElementById('docTypeBadge').textContent = svc ? 'Service' : 'Item';
document.getElementById('docTypeBadge').style.background = svc ? 'rgba(26,111,255,.15)' : 'rgba(4,126,126,.15)';
document.getElementById('docTypeBadge').style.color = svc ? 'var(--blue)' : 'var(--teal)';
document.getElementById('docTypeBadge').style.borderColor = svc ? 'rgba(26,111,255,.3)' : 'rgba(4,126,126,.3)';
document.getElementById('desc-req').style.display = svc ? '' : 'none';
// show/hide item-code and warehouse columns
document.querySelectorAll('.col-itemcode').forEach(el => el.style.display = svc ? 'none' : '');
document.querySelectorAll('.col-warehouse').forEach(el => el.style.display = svc ? 'none' : '');
// update all existing rows
document.querySelectorAll('#tbody tr').forEach(tr => {
const icCell = tr.querySelector('.cell-itemcode'); if (icCell) icCell.style.display = svc ? 'none' : '';
const whCell = tr.querySelector('.cell-warehouse'); if (whCell) whCell.style.display = svc ? 'none' : '';
const descInp = tr.querySelector('.desc-inp'); if (descInp) descInp.readOnly = !svc;
});
}
// ── 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);
});
}
// ── Tax Codes ──────────────────────────────────────────
async function loadTaxCodes(){
try {
const res = await fetch(`/api/sap/lookup/tax-codes?company=${encodeURIComponent(_co)}`, { headers: ah() });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const d = await res.json();
if (d.data?.length) _taxCodes = d.data;
else throw new Error('empty');
} catch {
_taxCodes = [
{Code:'CG+SG@0',Rate:0},{Code:'CG+SG@5',Rate:5},{Code:'CG+SG@12',Rate:12},
{Code:'CG+SG@18',Rate:18},{Code:'CG+SG@28',Rate:28},
{Code:'IGST@0',Rate:0},{Code:'IGST@5',Rate:5},{Code:'IGST@12',Rate:12},
{Code:'IGST@18',Rate:18},{Code:'IGST@28',Rate:28},
{Code:'EXEMPT',Rate:0},{Code:'NIL',Rate:0},{Code:'RIGST@5',Rate:5},
];
}
document.querySelectorAll('.taxcode-sel').forEach(s=>fillTaxSel(s));
}
function fillTaxSel(sel, defaultCode){
const prev = defaultCode || sel.value;
sel.innerHTML = '<option value="">— None —</option>' + _taxCodes.map(t=>
`<option value="${esc(t.Code)}" data-rate="${t.Rate||0}">${esc(t.Code)}</option>`
).join('');
if (prev){ const o=sel.querySelector(`option[value="${prev.replace(/"/g,'\\"')}"]`); if(o) sel.value=prev; }
sel.onchange = ()=>{
const opt = sel.options[sel.selectedIndex];
const rate = parseFloat(opt?.dataset?.rate||0);
const tp = sel.closest('tr')?.querySelector('.taxpct-inp');
if (tp){ tp.value=rate; recalc(); }
};
}
// ── 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(fillWhsSel);
} catch {}
}
function fillWhsSel(sel, defaultCode){
const prev = defaultCode || sel.value;
sel.innerHTML = '<option value="">— WH —</option>' + _whs.map(w=>
`<option value="${esc(w.WarehouseCode||w.code)}">${esc(w.WarehouseCode||w.code)}${(w.WarehouseName||w.name)?' — '+(w.WarehouseName||w.name):''}</option>`
).join('');
if (prev) sel.value = prev;
}
// ── Vendor autocomplete (inline dropdown) ─────────────
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');
toast(`Vendor: ${name}`, 'ok');
}
document.addEventListener('click', e=>{
if (!document.getElementById('vendorWrap').contains(e.target))
document.getElementById('vendorDd').classList.remove('open');
});
// ── Item autocomplete (fixed portal) ──────────────────
const portal = document.getElementById('acPortal');
const _itemTimers = {};
function positionPortal(inputEl){
const r = inputEl.getBoundingClientRect();
portal.style.left = r.left+'px';
portal.style.top = (r.bottom+2)+'px';
portal.style.width = Math.max(260, r.width)+'px';
}
document.addEventListener('mousedown', e=>{
if (!portal.contains(e.target) && !e.target.closest('.icode-inp')) closePortal();
});
window.addEventListener('scroll', ()=>{
if (portal.classList.contains('open') && _portalTarget) positionPortal(_portalTarget);
}, true);
function closePortal(){ portal.classList.remove('open'); portal.innerHTML=''; _portalTarget=null; }
function itemAcForInput(inputEl, lid){
const val = inputEl.value;
clearTimeout(_itemTimers[lid]);
if (val.length < 2){ closePortal(); return; }
portal.innerHTML = `<div class="aci"><div class="spin"></div><span class="ac-name" style="margin-left:6px">Searching…</span></div>`;
positionPortal(inputEl);
portal.classList.add('open');
_portalTarget = inputEl;
_itemTimers[lid] = setTimeout(async()=>{
try {
const res = await fetch(`/api/sap/lookup/items?q=${encodeURIComponent(val)}&company=${_co}`, { headers: ah() });
const data = await res.json();
const list = data.data || [];
portal.innerHTML = list.length
? list.map(i=>`<div class="aci" onmousedown="e => e.preventDefault()" onclick="selItem(${lid},'${esc(i.ItemCode)}','${esc((i.ItemName||'').replace(/'/g,"\\'"))}')"><span class="ac-name">${esc(i.ItemName)}</span><span class="ac-code">${esc(i.ItemCode)}</span></div>`).join('')
: '<div class="ac-msg">No items found</div>';
positionPortal(inputEl);
portal.classList.add('open');
} catch(err){ portal.innerHTML=`<div class="ac-msg">Error: ${esc(err.message)}</div>`; }
}, 300);
}
function selItem(lid, code, name){
const icode = document.getElementById(`icode-${lid}`);
const desc = document.getElementById(`desc-${lid}`);
if (icode) icode.value = code;
if (desc) desc.value = name;
closePortal();
recalc();
}
// ── Add / Remove / Renumber Lines ─────────────────────
function addLine(d={}){
lineSeq++;
const lid = lineSeq;
const svc = isService();
const rowNum = document.querySelectorAll('#tbody tr').length+1;
const tr = document.createElement('tr');
tr.id = `line-${lid}`;
tr.innerHTML = `
<td><span class="li-num">${rowNum}</span></td>
<td class="cell-itemcode" style="min-width:110px${svc?';display:none':''}">
<input class="icode-inp" type="text" id="icode-${lid}" placeholder="Item code…" value="${esc(d.itemCode||'')}"
autocomplete="off"
oninput="itemAcForInput(this,${lid})"
onfocus="if(this.value.length>=2)itemAcForInput(this,${lid})">
</td>
<td style="min-width:150px">
<input class="desc-inp" type="text" id="desc-${lid}" placeholder="${svc?'Service description…':'Description'}"
value="${esc(d.desc||'')}" ${svc?'':'readonly'}>
</td>
<td class="nc" style="width:70px">
<input class="qty-inp" type="number" id="qty-${lid}" value="${d.qty!==undefined?d.qty:1}" min="0" step="0.001" oninput="recalc()">
</td>
<td class="nc" style="width:100px">
<input class="price-inp" type="number" id="price-${lid}" value="${d.price!==undefined?d.price:0}" min="0" step="0.01" oninput="recalc()">
</td>
<td class="nc" style="width:60px">
<input class="disc-inp" type="number" id="disc-${lid}" value="${d.disc!==undefined?d.disc:0}" min="0" max="100" step="0.01" oninput="recalc()">
</td>
<td style="min-width:120px">
<select class="taxcode-sel" id="tax-${lid}"></select>
</td>
<td class="nc" style="width:55px">
<input class="taxpct-inp" type="number" id="tp-${lid}" value="${d.taxPct!==undefined?d.taxPct:0}" min="0" max="100" readonly>
</td>
<td class="nc" style="width:100px">
<input class="total-inp" type="number" id="tot-${lid}" value="0" readonly style="color:var(--teal);font-weight:600">
</td>
<td class="cell-warehouse" style="min-width:90px${svc?';display:none':''}">
<select class="whs-sel" id="whs-${lid}"></select>
</td>
<td><span class="xbtn" onclick="removeLine(${lid})">×</span></td>`;
document.getElementById('tbody').appendChild(tr);
fillTaxSel(tr.querySelector('.taxcode-sel'), d.taxCode||'');
if (!svc) fillWhsSel(tr.querySelector('.whs-sel'), d.warehouse||'');
renumber();
recalc();
}
function removeLine(lid){ document.getElementById(`line-${lid}`)?.remove(); renumber(); recalc(); }
function clearLines(){ document.getElementById('tbody').innerHTML=''; lineSeq=0; recalc(); }
function renumber(){
document.querySelectorAll('#tbody tr').forEach((tr,i)=>{
const s = tr.querySelector('.li-num'); if(s) s.textContent=i+1;
});
}
// ── Recalculate Totals ─────────────────────────────────
function recalc(){
let sub=0, disc=0, tax=0;
document.querySelectorAll('#tbody tr').forEach(tr=>{
const qty = parseFloat(tr.querySelector('.qty-inp')?.value ||0);
const price = parseFloat(tr.querySelector('.price-inp')?.value||0);
const discPct= parseFloat(tr.querySelector('.disc-inp')?.value ||0);
const taxPct = parseFloat(tr.querySelector('.taxpct-inp')?.value||0);
const base = qty * price;
const dAmt = base * discPct / 100;
const after = base - dAmt;
const tAmt = after * taxPct / 100;
sub += base; disc += dAmt; tax += tAmt;
const totInp = tr.querySelector('.total-inp');
if (totInp) totInp.value = (after + tAmt).toFixed(2);
});
document.getElementById('totSub').textContent = fmt(sub);
document.getElementById('totDisc').textContent = fmt(disc);
document.getElementById('totTax').textContent = fmt(tax);
document.getElementById('totDoc').textContent = fmt(sub - disc + tax);
}
// ── Build payload ──────────────────────────────────────
function buildPayload(){
const cardCode = document.getElementById('cardCode').value.trim();
const docDate = document.getElementById('docDate').value;
const docDueDate = document.getElementById('docDueDate').value;
const vendorRef = document.getElementById('vendorRef').value.trim();
const comments = document.getElementById('comments').value.trim();
const docType = document.getElementById('docType').value;
const svc = isService();
const lines = [];
document.querySelectorAll('#tbody tr').forEach(tr=>{
const itemCode = tr.querySelector('.icode-inp')?.value?.trim()||'';
const desc = tr.querySelector('.desc-inp')?.value?.trim()||'';
// for item type: itemCode required; for service type: desc required
if (!svc && !itemCode) return;
if (svc && !desc) return;
const line = {
ItemDescription: desc,
Quantity: parseFloat(tr.querySelector('.qty-inp')?.value||0),
UnitPrice: parseFloat(tr.querySelector('.price-inp')?.value||0),
};
if (!svc && itemCode) line.ItemCode = itemCode;
const disc = parseFloat(tr.querySelector('.disc-inp')?.value||0);
const taxCode = tr.querySelector('.taxcode-sel')?.value||'';
const whs = !svc ? (tr.querySelector('.whs-sel')?.value||'') : '';
if (disc > 0) line.DiscountPercent = disc;
if (taxCode) line.TaxCode = taxCode;
if (whs) line.WarehouseCode = whs;
lines.push(line);
});
const reqDate = document.getElementById('reqDate').value;
const payload = { company: _co, CardCode: cardCode, DocType: docType, DocumentLines: lines };
if (docDate) payload.DocDate = docDate;
if (docDueDate) payload.DocDueDate = docDueDate;
if (reqDate) payload.RequriedDate = reqDate;
if (vendorRef) payload.NumAtCard = vendorRef;
if (comments) payload.Comments = comments;
return payload;
}
// ── Submit ─────────────────────────────────────────────
async function submitPQ(){
const cardCode = document.getElementById('cardCode').value.trim();
if (!cardCode) return toast('Select a vendor first.', 'err');
if (!document.getElementById('docDate').value) return toast('Posting Date required.', 'err');
const rows = document.querySelectorAll('#tbody tr');
if (!rows.length) return toast('Add at least one line.', 'err');
const svc = isService();
let hasLine = false;
rows.forEach(tr=>{
if (svc) { if (tr.querySelector('.desc-inp')?.value?.trim()) hasLine=true; }
else { if (tr.querySelector('.icode-inp')?.value?.trim()) hasLine=true; }
});
if (!hasLine) return toast(svc ? 'Each service line needs a description.' : 'Each line needs an Item Code.', 'err');
spin(true, 'Posting to SAP…');
document.getElementById('postBtn').disabled = true;
document.getElementById('spPost').style.display = 'inline-block';
try {
const payload = buildPayload();
const res = await fetch('/api/sap/purchase-quotation', {
method:'POST', headers:{...ah(),'Content-Type':'application/json'},
body: JSON.stringify(payload),
});
const data = await res.json();
const result = data?.data||data;
if (res.ok && data?.success){
const docNum = result?.DocNum||result?.DocEntry||'—';
document.getElementById('docNumDisp').value = `#${docNum} · Open`;
toast(`Purchase Quotation Posted — Doc# ${docNum}`, 'ok');
} else {
const sapMsg = data?.error?.message?.value||data?.message||JSON.stringify(data).slice(0,200);
toast(`Error: ${sapMsg}`, 'err');
}
} catch(err){ toast('Network error: '+err.message, 'err'); }
finally {
spin(false);
document.getElementById('postBtn').disabled = false;
document.getElementById('spPost').style.display = 'none';
}
}
// ── Reset ──────────────────────────────────────────────
function resetForm(){
if (!confirm('Reset all fields?')) return;
['vendorSearch','cardCode','vendorRef','comments','reqDate'].forEach(id=>{
const el=document.getElementById(id); if(el) el.value='';
});
const today = new Date().toISOString().slice(0,10);
document.getElementById('docDate').value = today;
const validity = new Date(); validity.setDate(validity.getDate()+30);
document.getElementById('docDueDate').value = validity.toISOString().slice(0,10);
document.getElementById('docType').value = 'dDocument_Items';
document.getElementById('docNumDisp').value = 'AUTO · Draft';
onDocTypeChange();
clearLines(); addLine();
toast('Form reset.', 'info');
}
// ── View / Search ──────────────────────────────────────
function debounceSearch(){ clearTimeout(_srchTimer); _srchTimer=setTimeout(()=>{_page=0;loadQuotations();},400); }
async function loadQuotations(){
const co = document.getElementById('coSel2').value; // always read fresh from DOM
_co = co;
const wrap = document.getElementById('listWrap');
wrap.innerHTML = '<div class="pq-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-quotations?company=${encodeURIComponent(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();
if (!r.ok || d.success === false){
const msg = d.message||`HTTP ${r.status}`;
wrap.innerHTML=`<div class="pq-empty" style="color:var(--red)">&#10007; ${esc(msg)}</div>`;
document.getElementById('pgBar').style.display='none';
return;
}
const rows = d.value||d.data||[];
if (!rows.length){ wrap.innerHTML='<div class="pq-empty">No purchase quotations found.</div>'; document.getElementById('pgBar').style.display='none'; return; }
wrap.innerHTML = `<div class="pq-list">${rows.map(pq=>`
<div class="pq-card" onclick="openDoc(${pq.DocEntry})">
<div class="pq-card-top">
<span class="pq-docnum">#${esc(String(pq.DocNum||pq.DocEntry))}</span>
${statusBadge(pq.DocumentStatus)}
</div>
${pq.CardName?`<div class="pq-vendor" title="${esc(pq.CardName)}">${esc(pq.CardName)}${pq.CardCode?' ('+esc(pq.CardCode)+')':''}</div>`:''}
<div class="pq-meta">
<span>Date <b>${dt(pq.DocDate)}</b></span>
<span>Valid Until <b>${dt(pq.DocDueDate)}</b></span>
${pq.DocTotal!==undefined?`<span>Total <b>INR ${parseFloat(pq.DocTotal).toFixed(2)}</b></span>`:''}
</div>
</div>`).join('')}</div>`;
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="pq-empty" style="color:var(--red)">${esc(err.message)}</div>`; }
}
function pgMove(dir){ _page=Math.max(0,_page+dir); loadQuotations(); }
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>`;
return `<span class="badge s-draft">${esc(s||'Draft')}</span>`;
}
// ── Document detail modal ──────────────────────────────
async function openDoc(entry){
spin(true,'Loading document…');
try {
const r = await fetch(`/api/sap/purchase-quotations/${encodeURIComponent(entry)}?company=${encodeURIComponent(_co)}`, { headers: ah() });
const d = await r.json();
if (!r.ok) throw new Error(d?.message||'Not found');
_curDoc = d.data||d.value||d;
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('mTotal').textContent = doc.DocTotal!==undefined?'INR '+parseFloat(doc.DocTotal).toFixed(2):'—';
document.getElementById('mComments').textContent= doc.Comments||'—';
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 class="num">${l.Quantity||0}</td>
<td class="num">${parseFloat(l.UnitPrice||0).toFixed(2)}</td>
<td class="num">${parseFloat(l.DiscountPercent||0).toFixed(1)}%</td>
<td>${esc(l.TaxCode||'—')}</td>
<td class="num" style="color:var(--teal)">${parseFloat(l.LineTotal||0).toFixed(2)}</td>
<td>${esc(l.WarehouseCode||'—')}</td>
</tr>`).join('')
: '<tr><td colspan="9" style="text-align:center;padding:16px;color:var(--text3)">No lines</td></tr>';
const isOpen = s==='bost_Open';
const isClosed = s==='bost_Close';
document.getElementById('mActions').innerHTML = `
${isOpen ? `<button class="btn b-green btn-sm" onclick="docAction('close')">✓ Close</button>`:''}
${isOpen ? `<button class="btn b-warn btn-sm" onclick="docAction('cancel')">✕ Cancel</button>`:''}
${isClosed? `<button class="btn b-secondary btn-sm" onclick="docAction('reopen')">↩ Reopen</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();
});
async function docAction(action){
if (!_curDoc) return;
const entry = _curDoc.DocEntry;
const labels = { close:'Close', cancel:'Cancel', reopen:'Reopen' };
if (action==='cancel' && !confirm(`Cancel PQ #${_curDoc.DocNum}? This cannot be undone.`)) return;
spin(true, `${labels[action]||action}ing…`);
try {
const r = await fetch(`/api/sap/documents/PurchaseQuotations/${entry}/${action}?company=${encodeURIComponent(_co)}`,
{ method:'POST', headers:{...ah(),'Content-Type':'application/json'}, body:'{}' });
const d = await r.json();
if (!r.ok||!d.success) throw new Error(d?.message||'Action failed');
toast(`PQ #${_curDoc.DocNum} ${labels[action]||action}d successfully`, 'ok');
closeModal(); loadQuotations();
} catch(err){ toast(err.message,'err'); }
finally { spin(false); }
}
// ── Copy From (PR Chooser) ─────────────────────────────
let _selectedPR = null;
let _cfTimer = null;
function openCopyFrom(){
_selectedPR = null;
document.getElementById('btnChoose').disabled = true;
document.getElementById('cfSearch').value = '';
// sync _co from the visible selector right now before opening
_co = document.getElementById('coSel2').value;
document.getElementById('copyFromModal').classList.add('show');
loadPRList();
}
function closeCopyFrom(){ document.getElementById('copyFromModal').classList.remove('show'); _selectedPR=null; }
function cfDebounce(){ clearTimeout(_cfTimer); _cfTimer=setTimeout(loadPRList, 400); }
async function loadPRList(){
// always read company fresh from the visible selector — never rely on _co alone
const co = document.getElementById('coSel2').value;
if (co !== _co) _co = co; // keep _co in sync if it drifted
const tbody = document.getElementById('cfBody');
tbody.innerHTML = `<tr><td colspan="6" style="padding:24px;text-align:center;color:var(--text3)"><span class="sp" style="border-color:rgba(4,126,126,.25);border-top-color:var(--teal);width:16px;height:16px;border-width:2px"></span> Loading from <b>${esc(co)}</b>…</td></tr>`;
const q = document.getElementById('cfSearch').value.trim();
let url = `/api/sap/purchase-requests?company=${encodeURIComponent(co)}&$top=100&$skip=0&status=bost_Open`;
if (q) url += `&q=${encodeURIComponent(q)}`;
try {
const r = await fetch(url, { headers: ah() });
const d = await r.json();
const rows = (d.data || []).filter(pr => Number.isFinite(pr.DocEntry));
if (!rows.length){
tbody.innerHTML = `<tr><td colspan="6" style="padding:24px;text-align:center;color:var(--text3)">No open purchase requests found.</td></tr>`;
return;
}
tbody.innerHTML = rows.map((pr, i) => `
<tr class="cf-row" data-entry="${pr.DocEntry}" onclick="selectPR(this,${pr.DocEntry})" style="cursor:pointer;transition:background .1s">
<td style="padding:6px 8px;border-bottom:1px solid rgba(28,46,72,.4);color:var(--text3)">${i+1}</td>
<td style="padding:6px 8px;border-bottom:1px solid rgba(28,46,72,.4);font-family:var(--mono);color:var(--teal);font-weight:600">#${esc(String(pr.DocNum||pr.DocEntry))}</td>
<td style="padding:6px 8px;border-bottom:1px solid rgba(28,46,72,.4);font-family:var(--mono);font-size:10px">${dt(pr.DocDate)}</td>
<td style="padding:6px 8px;border-bottom:1px solid rgba(28,46,72,.4);font-family:var(--mono);font-size:10px">${dt(pr.RequriedDate||pr.DocDueDate)}</td>
<td style="padding:6px 8px;border-bottom:1px solid rgba(28,46,72,.4);font-size:11px">${esc(pr.CardName||pr.CardCode||'—')}</td>
<td style="padding:6px 8px;border-bottom:1px solid rgba(28,46,72,.4);color:var(--text2);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(pr.Comments||'')}</td>
</tr>`).join('');
} catch(err){
tbody.innerHTML = `<tr><td colspan="6" style="padding:24px;text-align:center;color:var(--red)">${esc(err.message)}</td></tr>`;
}
}
function selectPR(row, entry){
const id = Number.isFinite(entry) ? entry : parseInt(entry);
if (!Number.isFinite(id)) return; // guard: don't select rows with invalid DocEntry
document.querySelectorAll('.cf-row').forEach(r => { r.style.background=''; r.style.borderLeft=''; });
row.style.background = 'rgba(4,126,126,.14)';
row.style.borderLeft = '3px solid var(--teal)';
_selectedPR = id;
document.getElementById('btnChoose').disabled = false;
}
async function choosePR(){
if (!_selectedPR || !Number.isFinite(_selectedPR)) return;
const entryId = _selectedPR; // capture before closeCopyFrom() nulls it
const co = document.getElementById('coSel2').value;
spin(true, 'Loading Purchase Request…');
closeCopyFrom(); // resets _selectedPR → null (safe now)
try {
const r = await fetch(`/api/sap/purchase-requests/${entryId}?company=${encodeURIComponent(co)}`, { headers: ah() });
const d = await r.json();
if (!r.ok || !d.success) throw new Error(d.message||'Failed to load PR');
const pr = d.data;
// Clear existing lines first
clearLines();
// Populate header fields
if (pr.Comments) document.getElementById('comments').value = pr.Comments;
if (pr.RequriedDate||pr.DocDueDate)
document.getElementById('reqDate').value = (pr.RequriedDate||pr.DocDueDate||'').slice(0,10);
// Populate vendor if PR had one
if (pr.CardCode){
document.getElementById('cardCode').value = pr.CardCode;
document.getElementById('vendorSearch').value = pr.CardName
? `${pr.CardCode} — ${pr.CardName}`
: pr.CardCode;
}
// Populate lines from PR DocumentLines
const lines = pr.DocumentLines || [];
if (lines.length){
for (const l of lines){
addLine({
itemCode: l.ItemCode || '',
desc: l.ItemDescription || '',
qty: l.Quantity || 1,
price: l.UnitPrice || 0,
taxCode: l.TaxCode || '',
warehouse: l.WarehouseCode || '',
});
}
} else {
addLine();
}
toast(`Copied ${lines.length} line(s) from PR #${pr.DocNum||pr.DocEntry}`, 'ok');
} catch(err){
toast('Copy failed: ' + err.message, 'err');
if (!document.querySelectorAll('#tbody tr').length) addLine();
} 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;
const validity = new Date(); validity.setDate(validity.getDate()+30);
document.getElementById('docDueDate').value = validity.toISOString().slice(0,10);
// nav coSel → onCo('nav') so it reads from coSel (not coSel2)
document.getElementById('coSel').addEventListener('change', () => onCo('nav'));
// Copy From modal backdrop dismiss
document.getElementById('copyFromModal').addEventListener('click', e=>{
if (e.target === document.getElementById('copyFromModal')) closeCopyFrom();
});
// double-click row in Copy From to choose immediately
document.getElementById('cfBody').addEventListener('dblclick', e=>{
const row = e.target.closest('.cf-row');
if (!row) return;
selectPR(row, parseInt(row.dataset.entry));
if (Number.isFinite(_selectedPR)) choosePR();
});
await loadTaxCodes();
await loadWarehouses();
addLine();
})();
</script>
<script src="/sidebar.js?v=25"></script>
</body>
</html>