473 lines
28 KiB
HTML
473 lines
28 KiB
HTML
<!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>View Documents — SAP B1</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700;800&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;
|
|
--font:'Space Grotesk',sans-serif;
|
|
}
|
|
*{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}
|
|
main{padding:20px 24px 40px}
|
|
|
|
/* ── Cards ─────────────────────────────────────────────── */
|
|
.card{background:var(--surface);border:1px solid var(--border);border-radius:16px;margin-bottom:16px;box-shadow:0 1px 4px rgba(0,0,0,.05)}
|
|
.card-hd{padding:14px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px}
|
|
.card-hd-icon{width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,#ecfeff,#cffafe);display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
|
|
.card-hd-title{font-size:14px;font-weight:800;color:var(--text)}
|
|
.card-hd .right{margin-left:auto;font-size:11px;font-weight:700;color:var(--text3);background:var(--bg);padding:3px 10px;border-radius:20px;border:1px solid var(--border)}
|
|
.card-bd{padding:16px 20px}
|
|
|
|
/* ── Form ───────────────────────────────────────────────── */
|
|
.fg{margin-bottom:12px}
|
|
.fl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--text3);margin-bottom:5px;display:block}
|
|
.fi{width:100%;padding:9px 12px;border:1.5px solid var(--border);border-radius:9px;font-size:13px;font-family:var(--font);background:var(--surface);color:var(--text);outline:none;height:38px;transition:border .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)}
|
|
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 12px center;padding-right:30px}
|
|
.fi-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
|
@media(max-width:500px){.fi-row{grid-template-columns:1fr}}
|
|
|
|
/* ── Type chips ─────────────────────────────────────────── */
|
|
.type-tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:4px}
|
|
.type-tab{padding:6px 13px;border-radius:20px;font-size:11px;font-weight:700;color:var(--text3);cursor:pointer;border:1.5px solid var(--border);background:none;font-family:var(--font);transition:all .15s;white-space:nowrap;display:inline-flex;align-items:center;gap:5px}
|
|
.type-tab:hover{color:var(--text2);border-color:var(--border2);background:var(--bg)}
|
|
.type-tab.on{color:var(--teal);border-color:var(--teal3);background:#ecfeff}
|
|
|
|
/* ── Buttons ────────────────────────────────────────────── */
|
|
.btn{padding:9px 18px;border-radius:9px;border:none;font-family:var(--font);font-size:13px;font-weight:700;cursor:pointer;transition:all .18s;display:inline-flex;align-items:center;gap:6px}
|
|
.btn:disabled{opacity:.4;cursor:not-allowed}
|
|
.b-primary{background:linear-gradient(135deg,#047e7e,#035b5b);color:#fff}
|
|
.b-primary:hover:not(:disabled){box-shadow:0 4px 14px rgba(4,126,126,.35);transform:translateY(-1px)}
|
|
.b-cancel{background:var(--bg);color:var(--text2);border:1.5px solid var(--border)}
|
|
.b-cancel:hover:not(:disabled){border-color:var(--border2);background:#fff}
|
|
|
|
/* ── Document cards ─────────────────────────────────────── */
|
|
.doc-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:12px 16px 12px 20px;margin-bottom:8px;cursor:pointer;transition:all .18s;position:relative;overflow:hidden}
|
|
.doc-card::before{content:'';position:absolute;left:0;top:0;bottom:0;width:4px;border-radius:12px 0 0 12px}
|
|
.doc-card.purchase::before{background:var(--teal)}
|
|
.doc-card.sales::before{background:var(--blue)}
|
|
.doc-card.finance::before{background:var(--amber)}
|
|
.doc-card:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,.07);border-color:var(--teal3)}
|
|
.doc-top{display:flex;align-items:center;gap:6px;margin-bottom:4px;flex-wrap:wrap}
|
|
.doc-num{font-family:monospace;font-size:13px;color:var(--teal);font-weight:700;letter-spacing:.3px}
|
|
.doc-badge{font-size:9px;font-weight:700;padding:2px 7px;border-radius:6px;white-space:nowrap}
|
|
.doc-badge.open{background:#d1fae5;color:#065f46;border:1px solid #a7f3d0}
|
|
.doc-badge.closed{background:#fee2e2;color:#991b1b;border:1px solid #fca5a5}
|
|
.doc-badge.attach{background:#eff6ff;color:var(--blue);border:1px solid #bfdbfe}
|
|
.doc-name{font-size:13px;font-weight:600;color:var(--text);margin-bottom:3px}
|
|
.doc-meta{font-size:11px;color:var(--text3);display:flex;gap:10px;flex-wrap:wrap}
|
|
.doc-meta b{color:var(--text2);font-weight:600}
|
|
|
|
/* ── Empty / Spinner ────────────────────────────────────── */
|
|
.empty-msg{text-align:center;padding:36px;color:var(--text3);font-size:13px}
|
|
.sp{width:14px;height:14px;border:2px solid rgba(4,126,126,.25);border-top-color:var(--teal);border-radius:50%;animation:spin .6s linear infinite;display:inline-block;vertical-align:middle;margin-right:6px}
|
|
@keyframes spin{to{transform:rotate(360deg)}}
|
|
|
|
/* ── 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);display:flex;align-items:center;gap:8px;max-width:300px;pointer-events:auto;animation:toastIn .25s ease}
|
|
.t-ok{background:var(--green);color:#fff}.t-err{background:var(--red);color:#fff}
|
|
@keyframes toastIn{from{transform:translateY(12px);opacity:0}to{transform:translateY(0);opacity:1}}
|
|
|
|
/* ── 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:18px;width:100%;max-width:740px;margin:auto;box-shadow:0 24px 80px rgba(0,0,0,.18);animation:mIn .22s ease}
|
|
@keyframes mIn{from{transform:translateY(24px);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:12px}
|
|
.modal-hd-left{display:flex;align-items:center;gap:10px;min-width:0}
|
|
.modal-hd-icon{width:36px;height:36px;border-radius:9px;background:#ecfeff;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
|
|
.modal-hd-info{min-width:0}
|
|
.modal-hd-title{font-size:13px;font-weight:800;color:var(--text);line-height:1}
|
|
.modal-hd-num{font-family:monospace;font-size:12px;font-weight:700;color:var(--teal);margin-top:2px}
|
|
.modal-close{width:30px;height:30px;border-radius:8px;border:1.5px solid var(--border);background:var(--bg);color:var(--text3);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px;line-height:1;transition:all .15s;flex-shrink:0}
|
|
.modal-close:hover{background:#fee2e2;color:var(--red);border-color:#fca5a5}
|
|
.modal-bd{padding:16px 20px;max-height:74vh;overflow-y:auto}
|
|
.modal-bd::-webkit-scrollbar{width:4px}
|
|
.modal-bd::-webkit-scrollbar-thumb{background:var(--border2);border-radius:4px}
|
|
|
|
/* Detail rows */
|
|
.dt-section{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.6px;color:var(--text3);margin:14px 0 8px;padding-bottom:5px;border-bottom:2px solid var(--border)}
|
|
.dt-grid{display:grid;grid-template-columns:1fr 1fr;gap:0;border:1px solid var(--border);border-radius:10px;overflow:hidden;margin-bottom:4px}
|
|
.dt-row{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;flex-direction:column;gap:2px}
|
|
.dt-row:nth-last-child(-n+2){border-bottom:none}
|
|
.dt-row.full{grid-column:1/-1}
|
|
.dt-l{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:var(--text3)}
|
|
.dt-v{font-size:13px;font-weight:600;color:var(--text)}
|
|
|
|
/* Lines table */
|
|
.lines-wrap{border:1px solid var(--border);border-radius:10px;overflow:hidden;margin-top:4px}
|
|
.lines-tbl{width:100%;border-collapse:collapse;font-size:11px}
|
|
.lines-tbl th{background:#f9fafb;padding:7px 10px;text-align:left;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:var(--text3);border-bottom:1px solid var(--border);white-space:nowrap}
|
|
.lines-tbl td{padding:7px 10px;border-bottom:1px solid #f3f4f6;color:var(--text2);vertical-align:middle}
|
|
.lines-tbl tr:last-child td{border-bottom:none}
|
|
.lines-tbl tr:hover td{background:#f9fafb}
|
|
.td-mono{font-family:monospace;color:var(--teal);font-weight:600}
|
|
.td-right{text-align:right;font-family:monospace}
|
|
.td-grn{color:var(--green);font-family:monospace}
|
|
.td-red{color:var(--red);font-family:monospace}
|
|
|
|
/* Attachment */
|
|
.att-list{display:flex;flex-direction:column;gap:6px;margin-top:4px}
|
|
.att-row{display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--bg);border:1px solid var(--border);border-radius:10px}
|
|
.att-icon{font-size:22px;flex-shrink:0}
|
|
.att-info{flex:1;min-width:0}
|
|
.att-name{font-size:12px;font-weight:700;color:var(--text)}
|
|
.att-sub{font-size:10px;color:var(--text3);margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
.att-btn{padding:5px 13px;border-radius:7px;font-size:11px;font-weight:700;background:#eff6ff;color:var(--blue);border:1px solid #bfdbfe;cursor:pointer;font-family:var(--font);transition:all .15s;white-space:nowrap;flex-shrink:0}
|
|
.att-btn:hover{background:#dbeafe;border-color:#93c5fd}
|
|
|
|
/* Pagination */
|
|
.pag-row{display:flex;gap:8px;justify-content:center;margin-top:12px}
|
|
</style>
|
|
<link rel="stylesheet" href="/theme.css?v=3"/>
|
|
</head>
|
|
<body>
|
|
<div class="toast-c" id="toast-c"></div>
|
|
<div class="modal-bg" id="doc-modal"><div class="modal" id="modal-inner"></div></div>
|
|
<main id="app"></main>
|
|
|
|
<script>
|
|
'use strict';
|
|
let _tok=null,_user=null;
|
|
const BE='/api';
|
|
(function(){try{_tok=sessionStorage.getItem('portal_token');const u=sessionStorage.getItem('portal_user');if(u)_user=JSON.parse(u);}catch(_e){}})();
|
|
if(!_tok)window.location.href='/';
|
|
|
|
let _co=window.__DEFAULT_COMPANY__;
|
|
let _companies=[{value:_co,label:_co}]; // live list — populated at bootstrap
|
|
function coOptions(sel){return _companies.map(c=>`<option value="${esc(c.value)}" ${sel===c.value?'selected':''}>${esc(c.label)}</option>`).join('');}
|
|
let _docType='PurchaseRequests';
|
|
let _docs=[];
|
|
let _loading=false;
|
|
let _search='';
|
|
let _bpSearch='';
|
|
let _dateFrom='';
|
|
let _dateTo='';
|
|
let _docStatus='';
|
|
let _skip=0;
|
|
|
|
const TYPES=[
|
|
{key:'PurchaseRequests', label:'Purchase Request', icon:'', group:'Purchase'},
|
|
{key:'PurchaseOrders', label:'Purchase Order', icon:'', group:'Purchase'},
|
|
{key:'PurchaseDeliveryNotes',label:'GRPO', icon:'', group:'Purchase'},
|
|
{key:'PurchaseInvoices', label:'AP Invoice', icon:'', group:'Purchase'},
|
|
{key:'PurchaseCreditNotes',label:'AP Credit Note', icon:'', group:'Purchase'},
|
|
{key:'PurchaseReturns', label:'Goods Return', icon:'', group:'Purchase'},
|
|
{key:'Invoices', label:'AR Invoice', icon:'', group:'Sales'},
|
|
{key:'CreditNotes', label:'AR Credit Memo', icon:'', group:'Sales'},
|
|
{key:'Returns', label:'Return Notes', icon:'', group:'Sales'},
|
|
{key:'JournalEntries', label:'Journal Entry', icon:'', group:'Finance'},
|
|
];
|
|
|
|
function esc(v){return(v||'').toString().replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
|
function q(s){return document.querySelector(s);}
|
|
function fmtDate(d){if(!d)return'—';return new Date(d).toLocaleDateString('en-IN',{day:'2-digit',month:'short',year:'2-digit'});}
|
|
function fmtAmt(v,c){return(c||'INR')+' '+Number(v||0).toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2});}
|
|
|
|
async function api(m,p,b=null){
|
|
const h={'Content-Type':'application/json'};if(_tok)h['Authorization']='Bearer '+_tok;
|
|
const o={method:m.toUpperCase(),headers:h};if(b)o.body=JSON.stringify(b);
|
|
let r,d;try{r=await fetch(BE+p,o);d=await r.json();}catch(_e){throw new Error('Cannot reach server');}
|
|
if(r.status===401){window.location.href='/';throw new Error('Session expired');}
|
|
if(!r.ok)throw new Error(d?.message||'HTTP '+r.status);return d;
|
|
}
|
|
function toast(msg,type='ok'){
|
|
const tc=document.getElementById('toast-c');
|
|
const el=document.createElement('div');
|
|
el.className=`toast t-${type}`;
|
|
el.innerHTML=(type==='ok'?'✓':'✗')+' '+esc(msg);
|
|
tc.appendChild(el);setTimeout(()=>el.remove(),4000);
|
|
}
|
|
|
|
async function loadDocs(){
|
|
_loading=true;render();
|
|
try{
|
|
let qs=_search?`&q=${encodeURIComponent(_search)}`:'';
|
|
if(_bpSearch)qs+=`&bp=${encodeURIComponent(_bpSearch)}`;
|
|
if(_dateFrom)qs+=`&dateFrom=${_dateFrom}`;
|
|
if(_dateTo)qs+=`&dateTo=${_dateTo}`;
|
|
if(_docStatus)qs+=`&status=${_docStatus}`;
|
|
const r=await api('GET',`/sap/documents/${_docType}?company=${encodeURIComponent(_co)}&top=20&skip=${_skip}${qs}`);
|
|
_docs=r.data||[];
|
|
}catch(e){toast(e.message,'err');_docs=[];}
|
|
_loading=false;render();
|
|
}
|
|
|
|
function render(){
|
|
const app=document.getElementById('app');app.innerHTML='';
|
|
const typeCfg=TYPES.find(t=>t.key===_docType);
|
|
const grpCls=typeCfg?.group==='Sales'?'sales':typeCfg?.group==='Finance'?'finance':'purchase';
|
|
|
|
// ── Filter card ──────────────────────────────────────────────
|
|
const c1=document.createElement('div');c1.className='card';
|
|
c1.innerHTML=`
|
|
<div class="card-hd">
|
|
<div class="card-hd-icon"></div>
|
|
<span class="card-hd-title">View Documents</span>
|
|
</div>
|
|
<div class="card-bd">
|
|
<div class="fg">
|
|
<label class="fl">Company</label>
|
|
<select class="fi" id="sel-co">
|
|
${coOptions(_co)}
|
|
</select>
|
|
</div>
|
|
<div class="fg">
|
|
<label class="fl">Document Type</label>
|
|
<div class="type-tabs" id="type-tabs">
|
|
${TYPES.map(t=>`<button class="type-tab${_docType===t.key?' on':''}" data-dt="${t.key}">${t.label}</button>`).join('')}
|
|
</div>
|
|
</div>
|
|
<div class="fi-row">
|
|
<div class="fg"><label class="fl">Doc Number</label><input class="fi" id="inp-search" value="${esc(_search)}" placeholder="e.g. 123456" style="font-family:monospace"/></div>
|
|
<div class="fg"><label class="fl">Vendor / Customer</label><input class="fi" id="inp-bp" value="${esc(_bpSearch)}" placeholder="BP Code or Name…"/></div>
|
|
</div>
|
|
<div class="fi-row">
|
|
<div class="fg"><label class="fl">Date From</label><input class="fi" id="inp-datefrom" type="date" value="${_dateFrom}"/></div>
|
|
<div class="fg"><label class="fl">Date To</label><input class="fi" id="inp-dateto" type="date" value="${_dateTo}"/></div>
|
|
</div>
|
|
<div class="fg">
|
|
<label class="fl">Status</label>
|
|
<select class="fi" id="inp-status">
|
|
<option value="" ${_docStatus===''?'selected':''}>All</option>
|
|
<option value="O" ${_docStatus==='O'?'selected':''}>Open</option>
|
|
<option value="C" ${_docStatus==='C'?'selected':''}>Closed</option>
|
|
<option value="L" ${_docStatus==='L'?'selected':''}>Cancelled</option>
|
|
</select>
|
|
</div>
|
|
<div style="display:flex;gap:8px">
|
|
<button class="btn b-primary" id="btn-search" style="flex:1">Search</button>
|
|
<button class="btn b-cancel" id="btn-clear">Clear</button>
|
|
</div>
|
|
</div>`;
|
|
app.appendChild(c1);
|
|
|
|
// ── Results card ─────────────────────────────────────────────
|
|
const c2=document.createElement('div');c2.className='card';
|
|
let listHtml='';
|
|
if(_loading){
|
|
listHtml=`<div class="empty-msg"><span class="sp"></span>Loading documents…</div>`;
|
|
}else if(!_docs.length){
|
|
listHtml=`<div class="empty-msg">No documents found. Adjust filters and search again.</div>`;
|
|
}else{
|
|
const isJE=_docType==='JournalEntries';
|
|
const isPR=_docType==='PurchaseRequests';
|
|
listHtml=_docs.map(d=>{
|
|
const num=isJE?(d.Number||d.JdtNum):(d.DocNum||d.DocEntry);
|
|
const entry=isJE?d.JdtNum:d.DocEntry;
|
|
const date=isJE?fmtDate(d.ReferenceDate):fmtDate(d.DocDate);
|
|
const total=isJE?'':fmtAmt(d.DocTotal,d.DocCurrency);
|
|
const status=d.DocumentStatus==='bost_Close'?'closed':'open';
|
|
const hasAttach=d.AttachmentEntry&&d.AttachmentEntry>0;
|
|
const requiredDate=isPR&&d.RequriedDate?fmtDate(d.RequriedDate):'';
|
|
return`<div class="doc-card ${grpCls}" data-entry="${entry}" data-type="${_docType}">
|
|
<div class="doc-top">
|
|
<span class="doc-num">#${num}</span>
|
|
${!isJE?`<span class="doc-badge ${status}">${status==='closed'?'Closed':'Open'}</span>`:''}
|
|
${hasAttach?'<span class="doc-badge attach">Attachment</span>':''}
|
|
</div>
|
|
<div class="doc-name">${esc(isJE?(d.Memo||'—'):(d.CardName||d.CardCode||'— No Vendor —'))}</div>
|
|
<div class="doc-meta">
|
|
<span>Date: <b>${date}</b></span>
|
|
${isPR&&requiredDate?`<span>Required: <b>${requiredDate}</b></span>`:''}
|
|
${!isJE&&!isPR?`<span>Total: <b>${total}</b></span>`:''}
|
|
${isPR&&d.DocTotal?`<span>Est. Total: <b>${total}</b></span>`:''}
|
|
${!isJE&&d.CardCode?`<span>BP: <b>${esc(d.CardCode)}</b></span>`:''}
|
|
${isJE&&d.Reference?`<span>Ref: <b>${esc(d.Reference)}</b></span>`:''}
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
c2.innerHTML=`
|
|
<div class="card-hd">
|
|
<div class="card-hd-icon"></div>
|
|
<span class="card-hd-title">${typeCfg?.label||_docType}</span>
|
|
<span class="right">${_docs.length} doc${_docs.length!==1?'s':''}</span>
|
|
</div>
|
|
<div class="card-bd">
|
|
${listHtml}
|
|
${!_loading&&_docs.length>=20?`<div class="pag-row">
|
|
${_skip>0?`<button class="btn b-cancel" id="btn-prev">← Prev</button>`:''}
|
|
<button class="btn b-cancel" id="btn-next">Next 20 →</button>
|
|
</div>`:''}
|
|
</div>`;
|
|
app.appendChild(c2);
|
|
|
|
// ── Wire events ──────────────────────────────────────────────
|
|
setTimeout(()=>{
|
|
q('#sel-co')&&(q('#sel-co').onchange=function(){_co=this.value;_skip=0;loadDocs();});
|
|
document.querySelectorAll('[data-dt]').forEach(b=>{b.onclick=function(){_docType=this.dataset.dt;_skip=0;_search='';loadDocs();};});
|
|
const doSearch=()=>{
|
|
_search=(q('#inp-search')?.value||'').trim();
|
|
_bpSearch=(q('#inp-bp')?.value||'').trim();
|
|
_dateFrom=q('#inp-datefrom')?.value||'';
|
|
_dateTo=q('#inp-dateto')?.value||'';
|
|
_docStatus=q('#inp-status')?.value||'';
|
|
_skip=0;loadDocs();
|
|
};
|
|
q('#btn-search')&&(q('#btn-search').onclick=doSearch);
|
|
q('#btn-clear')&&(q('#btn-clear').onclick=()=>{_search='';_bpSearch='';_dateFrom='';_dateTo='';_docStatus='';_skip=0;loadDocs();});
|
|
q('#inp-search')&&(q('#inp-search').onkeydown=e=>{if(e.key==='Enter')doSearch();});
|
|
q('#inp-bp')&&(q('#inp-bp').onkeydown=e=>{if(e.key==='Enter')doSearch();});
|
|
q('#btn-prev')&&(q('#btn-prev').onclick=()=>{_skip=Math.max(0,_skip-20);loadDocs();});
|
|
q('#btn-next')&&(q('#btn-next').onclick=()=>{_skip+=20;loadDocs();});
|
|
document.querySelectorAll('[data-entry]').forEach(c=>{c.onclick=function(){openDetail(this.dataset.type,parseInt(this.dataset.entry));};});
|
|
},0);
|
|
}
|
|
|
|
async function openDetail(type,entry){
|
|
const modal=document.getElementById('doc-modal'),inner=document.getElementById('modal-inner');
|
|
modal.classList.add('show');
|
|
inner.innerHTML=`<div style="padding:48px;text-align:center;color:var(--text3)"><span class="sp"></span>Loading…</div>`;
|
|
try{
|
|
const r=await api('GET',`/sap/documents/${type}/${entry}?company=${encodeURIComponent(_co)}`);
|
|
const d=r.data;
|
|
const isJE=type==='JournalEntries';
|
|
const isPR=type==='PurchaseRequests';
|
|
const typeCfg=TYPES.find(t=>t.key===type);
|
|
const num=isJE?(d.Number||d.JdtNum):(d.DocNum||d.DocEntry);
|
|
const lines=isJE?(d.JournalEntryLines||[]):(d.DocumentLines||[]);
|
|
|
|
// ── Lines table ──────────────────────────────────────────
|
|
let linesHtml='';
|
|
if(lines.length){
|
|
if(isJE){
|
|
linesHtml=`<div class="lines-wrap"><table class="lines-tbl">
|
|
<thead><tr><th>Account</th><th>Memo</th><th class="td-right">Debit</th><th class="td-right">Credit</th></tr></thead>
|
|
<tbody>${lines.map(l=>`<tr>
|
|
<td class="td-mono">${esc(l.AccountCode)}</td>
|
|
<td>${esc(l.LineMemo||'')}</td>
|
|
<td class="td-grn td-right">${l.Debit?fmtAmt(l.Debit):''}</td>
|
|
<td class="td-red td-right">${l.Credit?fmtAmt(l.Credit):''}</td>
|
|
</tr>`).join('')}</tbody>
|
|
</table></div>`;
|
|
}else if(isPR){
|
|
linesHtml=`<div class="lines-wrap"><table class="lines-tbl">
|
|
<thead><tr><th>Item Code</th><th>Description</th><th class="td-right">Qty</th><th>Whse</th><th>Vendor</th><th class="td-right">Req. Date</th></tr></thead>
|
|
<tbody>${lines.map(l=>`<tr>
|
|
<td class="td-mono">${esc(l.ItemCode||'')}</td>
|
|
<td>${esc(l.ItemDescription||'')}</td>
|
|
<td class="td-right">${l.Quantity||''}</td>
|
|
<td style="color:var(--text3)">${esc(l.WarehouseCode||'')}</td>
|
|
<td>${esc(l.LineVendor||'—')}</td>
|
|
<td class="td-right" style="color:var(--text3)">${l.RequriedDate?fmtDate(l.RequriedDate):'—'}</td>
|
|
</tr>`).join('')}</tbody>
|
|
</table></div>`;
|
|
}else{
|
|
linesHtml=`<div class="lines-wrap"><table class="lines-tbl">
|
|
<thead><tr><th>Item Code</th><th>Description</th><th class="td-right">Qty</th><th>Whse</th><th class="td-right">Unit Price</th><th class="td-right">Total</th></tr></thead>
|
|
<tbody>${lines.map(l=>`<tr>
|
|
<td class="td-mono">${esc(l.ItemCode||'')}</td>
|
|
<td>${esc(l.ItemDescription||'')}</td>
|
|
<td class="td-right">${l.Quantity||''}</td>
|
|
<td style="color:var(--text3)">${esc(l.WarehouseCode||'')}</td>
|
|
<td class="td-right" style="color:var(--text3)">${l.UnitPrice?fmtAmt(l.UnitPrice):'—'}</td>
|
|
<td class="td-right" style="font-weight:600;color:var(--text)">${l.LineTotal?fmtAmt(l.LineTotal):''}</td>
|
|
</tr>`).join('')}</tbody>
|
|
</table></div>`;
|
|
}
|
|
}else{
|
|
linesHtml=`<div class="empty-msg" style="padding:16px">No lines</div>`;
|
|
}
|
|
|
|
// ── Attachments ──────────────────────────────────────────
|
|
let attachHtml='';
|
|
if(!isJE&&d.AttachmentEntry&&d.AttachmentEntry>0){
|
|
try{
|
|
const att=await api('GET',`/sap/attachments/${d.AttachmentEntry}?company=${encodeURIComponent(_co)}`);
|
|
const attLines=att.data?.Attachments2_Lines||[];
|
|
if(attLines.length){
|
|
attachHtml=`<div class="dt-section">Attachments (${attLines.length})</div>
|
|
<div class="att-list">${attLines.map((a,i)=>`<div class="att-row">
|
|
<span class="att-icon"></span>
|
|
<div class="att-info">
|
|
<div class="att-name">${esc(a.FileName||'')}.${esc(a.FileExtension||'')}</div>
|
|
<div class="att-sub">${esc(a.SourcePath||'')}</div>
|
|
</div>
|
|
<button class="att-btn" onclick="viewAttachment(${d.AttachmentEntry},${a.LineNum||i})">View</button>
|
|
</div>`).join('')}</div>`;
|
|
}
|
|
}catch(_e){}
|
|
}
|
|
|
|
// ── Detail rows ──────────────────────────────────────────
|
|
let detailRows='';
|
|
if(isJE){
|
|
detailRows=`<div class="dt-grid">
|
|
<div class="dt-row"><span class="dt-l">Number</span><span class="dt-v">${d.Number||d.JdtNum}</span></div>
|
|
<div class="dt-row"><span class="dt-l">Date</span><span class="dt-v">${fmtDate(d.ReferenceDate)}</span></div>
|
|
<div class="dt-row"><span class="dt-l">Due Date</span><span class="dt-v">${fmtDate(d.DueDate)}</span></div>
|
|
<div class="dt-row"><span class="dt-l">Reference</span><span class="dt-v">${esc(d.Reference||'—')}</span></div>
|
|
<div class="dt-row full"><span class="dt-l">Memo</span><span class="dt-v">${esc(d.Memo||'—')}</span></div>
|
|
</div>`;
|
|
}else if(isPR){
|
|
detailRows=`<div class="dt-grid">
|
|
<div class="dt-row"><span class="dt-l">Doc Number</span><span class="dt-v">${d.DocNum||d.DocEntry}</span></div>
|
|
<div class="dt-row"><span class="dt-l">Status</span><span class="dt-v">${d.DocumentStatus==='bost_Close'?'Closed':'Open'}</span></div>
|
|
<div class="dt-row"><span class="dt-l">Date</span><span class="dt-v">${fmtDate(d.DocDate)}</span></div>
|
|
<div class="dt-row"><span class="dt-l">Required Date</span><span class="dt-v">${fmtDate(d.RequriedDate)}</span></div>
|
|
<div class="dt-row full"><span class="dt-l">Requester</span><span class="dt-v">${esc(d.CardName||d.CardCode||'— Internal Request —')}</span></div>
|
|
<div class="dt-row full"><span class="dt-l">Comments</span><span class="dt-v" style="color:var(--text3)">${esc(d.Comments||'—')}</span></div>
|
|
</div>`;
|
|
}else{
|
|
detailRows=`<div class="dt-grid">
|
|
<div class="dt-row"><span class="dt-l">Doc Number</span><span class="dt-v">${d.DocNum||d.DocEntry}</span></div>
|
|
<div class="dt-row"><span class="dt-l">Status</span><span class="dt-v">${d.DocumentStatus==='bost_Close'?'Closed':'Open'}</span></div>
|
|
<div class="dt-row"><span class="dt-l">Date</span><span class="dt-v">${fmtDate(d.DocDate)}</span></div>
|
|
<div class="dt-row"><span class="dt-l">Due Date</span><span class="dt-v">${fmtDate(d.DocDueDate)}</span></div>
|
|
<div class="dt-row full"><span class="dt-l">Business Partner</span><span class="dt-v">${esc(d.CardName||d.CardCode||'—')}</span></div>
|
|
<div class="dt-row"><span class="dt-l">Total</span><span class="dt-v" style="color:var(--teal);font-weight:700">${fmtAmt(d.DocTotal,d.DocCurrency)}</span></div>
|
|
<div class="dt-row full"><span class="dt-l">Comments</span><span class="dt-v" style="color:var(--text3)">${esc(d.Comments||'—')}</span></div>
|
|
</div>`;
|
|
}
|
|
|
|
inner.innerHTML=`
|
|
<div class="modal-hd">
|
|
<div class="modal-hd-left">
|
|
<div class="modal-hd-icon"></div>
|
|
<div class="modal-hd-info">
|
|
<div class="modal-hd-title">${typeCfg?.label||type}</div>
|
|
<div class="modal-hd-num">#${num}</div>
|
|
</div>
|
|
</div>
|
|
<button class="modal-close" id="m-close">×</button>
|
|
</div>
|
|
<div class="modal-bd">
|
|
<div class="dt-section">Document Details</div>
|
|
${detailRows}
|
|
<div class="dt-section">Lines (${lines.length})</div>
|
|
${linesHtml}
|
|
${attachHtml}
|
|
</div>`;
|
|
q('#m-close').onclick=()=>modal.classList.remove('show');
|
|
}catch(e){
|
|
inner.innerHTML=`<div style="padding:32px;text-align:center;color:var(--red)">✗ ${esc(e.message)}</div>`;
|
|
}
|
|
}
|
|
|
|
window.viewAttachment=function(entry,lineId){
|
|
const url=BE+`/sap/attachments/${entry}/download/${lineId}?company=${encodeURIComponent(_co)}`;
|
|
fetch(url,{headers:{'Authorization':'Bearer '+_tok}})
|
|
.then(r=>{if(!r.ok)throw new Error('File not accessible');return r.blob();})
|
|
.then(blob=>{const burl=URL.createObjectURL(blob);window.open(burl,'_blank');})
|
|
.catch(e=>{toast('Cannot open: '+e.message,'err');});
|
|
};
|
|
|
|
(async function(){try{const list=await window.fetchCompanyList();if(list.length)_companies=list;}catch(_e){}loadDocs();})();
|
|
</script>
|
|
<script src="/sidebar.js?v=25"></script>
|
|
</body>
|
|
</html>
|