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

This commit is contained in:
John
2026-09-23 17:31:02 +05:30
commit 69b4e68baf
51657 changed files with 3864077 additions and 0 deletions
+484
View File
@@ -0,0 +1,484 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/auth-guard.js?v=4"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Reports | MITRA ERP Portal</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Space Grotesk',sans-serif;background:#f0f2f5;color:#1a1a2e;min-height:100vh}
.pg{padding:20px 24px}
.hdr{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:18px}
.hdr h1{font-size:20px;font-weight:700;color:#1a1a2e}.hdr h1 span{color:#047e7e}
.btn{height:36px;padding:0 16px;border-radius:8px;font-size:12px;font-weight:700;font-family:inherit;border:none;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
.btn-primary{background:#047e7e;color:#fff}.btn-primary:hover{background:#035b5b}
.btn-primary:disabled{background:#b0bec5;cursor:not-allowed;opacity:.7}
.btn-outline{background:#fff;color:#047e7e;border:1.5px solid #047e7e}.btn-outline:hover{background:#f0fafa}
.btn-outline:disabled{opacity:.5;cursor:not-allowed}
.btn-grp{display:flex;gap:8px;flex-wrap:wrap}
.card{background:#fff;border-radius:14px;box-shadow:0 1px 8px rgba(0,0,0,.08);overflow:hidden}
.filter-bar{display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap;padding:16px 20px;border-bottom:1px solid #e8eaed;background:#fff}
.fg{display:flex;flex-direction:column;gap:5px}
.fg label{font-size:10px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.6px}
.fg input,.fg select{height:36px;border:1.5px solid #dde1e7;border-radius:8px;padding:0 12px;font-size:13px;font-family:inherit;color:#1a1a2e;background:#fff;outline:none;transition:border-color .15s}
.fg input:focus,.fg select:focus{border-color:#047e7e;box-shadow:0 0 0 3px rgba(4,126,126,.08)}
.summary-chips{display:none;gap:8px;flex-wrap:wrap;padding:12px 20px;border-bottom:1px solid #e8eaed;background:#f8fafb}
.chip{background:#fff;border:1.5px solid #c8e6e6;border-radius:10px;padding:6px 14px;font-size:11px;font-weight:600;color:#047e7e;min-width:100px}
.chip span{font-size:15px;font-weight:700;display:block;color:#1a1a2e;margin-top:1px}
.chip.blue{border-color:#bfdbfe;color:#1d4ed8}
.chip.green{border-color:#bbf7d0;color:#166534}
.chip.amber{border-color:#fde68a;color:#92400e}
.chip.red{border-color:#fecaca;color:#991b1b}
.chip.purple{border-color:#e9d5ff;color:#6d28d9}
.tbl-wrap{overflow-x:auto;overflow-y:auto;max-height:54vh}
.tbl-wrap::-webkit-scrollbar{height:7px;width:7px}
.tbl-wrap::-webkit-scrollbar-thumb{background:#c1c7ce;border-radius:4px}
table{border-collapse:collapse;min-width:100%;width:100%}
thead th{background:#f0f4f8;color:#4b5563;font-weight:700;padding:9px 11px;text-align:left;white-space:nowrap;position:sticky;top:0;z-index:2;border-bottom:2px solid #d1dae3;font-size:10px;text-transform:uppercase;letter-spacing:.5px}
thead th.num{text-align:right}
tbody tr{border-bottom:1px solid #f0f2f4;transition:background .1s}
tbody tr:hover{background:#edfafa}
tbody td{padding:8px 11px;white-space:nowrap;color:#374151;font-size:12px}
tbody td.num{text-align:right;font-variant-numeric:tabular-nums}
tbody td.trunc{max-width:200px;overflow:hidden;text-overflow:ellipsis}
.empty{text-align:center;padding:56px 20px;color:#9ca3af;font-size:13px}
.status-bar{display:flex;align-items:center;gap:10px;padding:10px 20px;background:#f8fafb;border-top:1px solid #e8eaed;font-size:12px;color:#6b7280;border-radius:0 0 14px 14px}
.status-dot{width:8px;height:8px;border-radius:50%;background:#d1d5db;flex-shrink:0}
.status-dot.ok{background:#10b981}.status-dot.err{background:#ef4444}
.status-dot.loading{background:#f59e0b;animation:pulse 1s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.badge{display:inline-flex;align-items:center;padding:2px 10px;border-radius:12px;font-size:10px;font-weight:700}
.badge-released{background:#dcfce7;color:#166534}
.badge-planned{background:#dbeafe;color:#1e40af}
.over-due{color:#dc2626;font-weight:700}
.report-tabs{display:flex;gap:4px;padding:12px 20px 0;background:#fff;border-bottom:1px solid #e8eaed;overflow-x:auto}
.report-tabs::-webkit-scrollbar{height:0}
.tab-btn{padding:8px 16px;border:none;background:none;cursor:pointer;font-family:inherit;font-size:12px;font-weight:700;color:#6b7280;border-bottom:2px solid transparent;margin-bottom:-1px;white-space:nowrap;transition:all .15s}
.tab-btn.active{color:#047e7e;border-bottom-color:#047e7e}
.tab-btn:hover:not(.active){color:#1a1a2e}
</style>
<link rel="stylesheet" href="/theme.css?v=3"/>
</head>
<body>
<div id="sidebar-placeholder"></div>
<div class="sb-main">
<div class="pg">
<div class="hdr">
<h1>Reports <span id="reportTitle">— Open Production Orders</span></h1>
<div class="btn-grp">
<button class="btn btn-outline" id="btnExport" disabled onclick="exportCsv()">Export CSV</button>
</div>
</div>
<div class="card">
<!-- ── Tabs ── -->
<div class="report-tabs">
<button class="tab-btn active" onclick="switchReport('prod',this)">Open Production Orders</button>
<button class="tab-btn" onclick="switchReport('inv',this)">FG Inventory</button>
<button class="tab-btn" onclick="switchReport('bom',this)">BOM Cost</button>
<button class="tab-btn" onclick="switchReport('sales',this)">Sales Report</button>
</div>
<!-- ── Filters: Production Orders ── -->
<div id="filters-prod" class="filter-bar">
<div class="fg"><label>From Date</label><input type="date" id="prod-from"></div>
<div class="fg"><label>To Date</label><input type="date" id="prod-to"></div>
<div class="fg">
<label>Status</label>
<select id="prod-status">
<option value="all">All (Released + Planned)</option>
<option value="Released">Released only</option>
<option value="Planned">Planned only</option>
</select>
</div>
<div class="fg">
<label>Product Segment</label>
<select id="prod-segment">
<option value="all">All Segments</option>
<option>BLOOD BAG</option><option>FG Blood Bag</option><option>SFG Blood bag</option>
<option>CAPD</option><option>FG CAPD</option><option>FG CAPD ACCESSORIES</option>
<option>Equipment</option><option>FG Equipment</option><option>Consumable</option>
<option>NEEDLE</option><option>MOULDING</option><option>PLASTIC</option>
<option>FG Stent</option><option>FG TT Device</option><option>Other</option>
</select>
</div>
<button class="btn btn-primary" onclick="loadData()">Load Data</button>
</div>
<!-- ── Filters: FG Inventory ── -->
<div id="filters-inv" class="filter-bar" style="display:none">
<div class="fg"><label>From Date</label><input type="date" id="inv-from"></div>
<div class="fg"><label>To Date</label><input type="date" id="inv-to"></div>
<div class="fg">
<label>Warehouse Code</label>
<input type="text" id="inv-whs" value="01" style="width:100px" placeholder="e.g. 01">
</div>
<div class="fg">
<label>Item Group</label>
<select id="inv-grp">
<option>FG BLOOD BAG</option>
<option>FG CAPD</option>
<option>FG CAPD Accessories</option>
<option>FG EQUIPMENT</option>
<option>SEMI FINISHED</option>
</select>
</div>
<button class="btn btn-primary" onclick="loadData()">Load Data</button>
</div>
<!-- ── Filters: BOM Cost ── -->
<div id="filters-bom" class="filter-bar" style="display:none">
<div class="fg">
<label>Item Group</label>
<select id="bom-grp">
<option>FG BLOOD BAG</option>
<option>FG CAPD</option>
<option>FG CAPD Accessories</option>
</select>
</div>
<button class="btn btn-primary" onclick="loadData()">Load Data</button>
</div>
<!-- ── Filters: Sales Report ── -->
<div id="filters-sales" class="filter-bar" style="display:none">
<div class="fg">
<label>As Of Date <span style="font-size:9px;color:#9ca3af;font-weight:400">(reference date)</span></label>
<input type="date" id="sales-asof">
</div>
<div class="fg">
<label>Period</label>
<select id="sales-period">
<option value="all">All (Daily + Monthly + Yearly)</option>
<option value="daily">Daily</option>
<option value="monthly">Monthly</option>
<option value="yearly">Yearly</option>
</select>
</div>
<button class="btn btn-primary" onclick="loadData()">Load Data</button>
</div>
<!-- ── Summary chips ── -->
<div class="summary-chips" id="chips"></div>
<!-- ── Table ── -->
<div class="tbl-wrap" id="tblWrap">
<div class="empty">Select filters and click <strong>Load Data</strong></div>
</div>
<div class="status-bar">
<div class="status-dot" id="statusDot"></div>
<span id="statusMsg">Ready</span>
</div>
</div>
</div>
</div>
<script>
(function(){
const tok = ()=> sessionStorage.getItem('portal_token')||'';
const authH = ()=>({'Content-Type':'application/json','Authorization':'Bearer '+tok()});
const fmtN = v => (parseFloat(v)||0).toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2});
const fmtC = v => '₹'+fmtN(v);
let currentReport = 'prod';
let currentData = [];
const TITLES = {
prod: '— Open Production Orders',
inv: '— FG Inventory',
bom: '— BOM Cost',
sales: '— Sales Report',
};
// ── Set defaults ─────────────────────────────────────────────────────────
(function(){
const today = new Date();
const y15 = new Date(); y15.setDate(y15.getDate()-14);
const m1 = new Date(today.getFullYear(), today.getMonth(), 1);
const yday = new Date(); yday.setDate(yday.getDate()-1);
document.getElementById('prod-from').value = y15.toISOString().split('T')[0];
document.getElementById('prod-to').value = today.toISOString().split('T')[0];
document.getElementById('inv-from').value = m1.toISOString().split('T')[0];
document.getElementById('inv-to').value = today.toISOString().split('T')[0];
document.getElementById('sales-asof').value = yday.toISOString().split('T')[0];
})();
function setStatus(msg, type=''){
document.getElementById('statusMsg').textContent = msg;
document.getElementById('statusDot').className = 'status-dot'+(type?' '+type:'');
}
function switchReport(name, el){
currentReport = name;
currentData = [];
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
el.classList.add('active');
document.getElementById('reportTitle').textContent = TITLES[name]||'';
['prod','inv','bom','sales'].forEach(r=>{
document.getElementById('filters-'+r).style.display = r===name ? 'flex' : 'none';
});
document.getElementById('chips').style.display = 'none';
document.getElementById('chips').innerHTML = '';
document.getElementById('tblWrap').innerHTML = '<div class="empty">Click <strong>Load Data</strong> to fetch</div>';
document.getElementById('btnExport').disabled = true;
setStatus('Ready');
}
// ── Generic loader ────────────────────────────────────────────────────────
async function loadData(){
document.getElementById('btnExport').disabled = true;
document.getElementById('tblWrap').innerHTML = '<div class="empty">Loading…</div>';
document.getElementById('chips').style.display= 'none';
setStatus('Fetching from SAP B1…','loading');
try {
let url, data;
if (currentReport === 'prod') { url = buildProdUrl(); }
else if (currentReport === 'inv') { url = buildInvUrl(); }
else if (currentReport === 'bom') { url = buildBomUrl(); }
else if (currentReport === 'sales') { url = buildSalesUrl(); }
const r = await fetch(url, {headers: authH()});
const j = await r.json();
if (!j.success) throw new Error(j.message);
currentData = j.data || [];
renderReport(j);
setStatus(`${currentData.length} record${currentData.length!==1?'s':''} loaded`, 'ok');
document.getElementById('btnExport').disabled = currentData.length === 0;
} catch(e) {
setStatus(e.message, 'err');
document.getElementById('tblWrap').innerHTML = `<div class="empty" style="color:#ef4444">${e.message}</div>`;
}
}
function buildProdUrl(){
const from = document.getElementById('prod-from').value;
const to = document.getElementById('prod-to').value;
const status = document.getElementById('prod-status').value;
const segment = document.getElementById('prod-segment').value;
if (!from||!to) throw new Error('Select from and to dates');
return `/api/reports/open-production-orders?from=${from}&to=${to}&status=${status}&segment=${encodeURIComponent(segment)}`;
}
function buildInvUrl(){
const from = document.getElementById('inv-from').value;
const to = document.getElementById('inv-to').value;
const whs = document.getElementById('inv-whs').value.trim() || '01';
const grp = document.getElementById('inv-grp').value;
if (!from||!to) throw new Error('Select from and to dates');
return `/api/reports/fg-inventory?from=${from}&to=${to}&warehouse=${encodeURIComponent(whs)}&itemGroup=${encodeURIComponent(grp)}`;
}
function buildBomUrl(){
const grp = document.getElementById('bom-grp').value;
return `/api/reports/bom-cost?itemGroup=${encodeURIComponent(grp)}`;
}
function buildSalesUrl(){
const asOf = document.getElementById('sales-asof').value;
const period = document.getElementById('sales-period').value;
if (!asOf) throw new Error('Select a reference date');
return `/api/reports/sales?asOf=${asOf}&period=${period}`;
}
// ── Dispatch renderer ─────────────────────────────────────────────────────
function renderReport(j){
if (currentReport === 'prod') renderProd(j.data);
else if (currentReport === 'inv') renderInv(j.data);
else if (currentReport === 'bom') renderBom(j.data, j.itemGroup);
else if (currentReport === 'sales') renderSales(j.data);
}
// ── Chips helper ──────────────────────────────────────────────────────────
function showChips(items){
const box = document.getElementById('chips');
box.innerHTML = items.map(({label,value,cls})=>
`<div class="chip ${cls||''}"><div>${label}</div><span>${value}</span></div>`
).join('');
box.style.display = 'flex';
}
// ── Render table helper ───────────────────────────────────────────────────
function renderTable(cols, rows){
if (!rows.length){
document.getElementById('tblWrap').innerHTML = '<div class="empty">No data found</div>';
return;
}
const thead = '<thead><tr>' + cols.map(c=>`<th class="${c.css||''}">${c.l}</th>`).join('') + '</tr></thead>';
const tbody = '<tbody>' + rows.map(row =>
'<tr>' + cols.map(c => {
let v = row[c.k] ?? '';
if (c.render) return c.render(v, row);
if (c.css === 'num' && v !== '') v = fmtN(v);
return `<td class="${c.css||''}" title="${String(v).replace(/"/g,'&quot;')}">${v}</td>`;
}).join('') + '</tr>'
).join('') + '</tbody>';
document.getElementById('tblWrap').innerHTML = `<table>${thead}${tbody}</table>`;
}
// ── PRODUCTION ORDERS ─────────────────────────────────────────────────────
function renderProd(rows){
if (!rows.length){
document.getElementById('tblWrap').innerHTML = '<div class="empty">No open production orders found</div>';
return;
}
const today = new Date(); today.setHours(0,0,0,0);
const released = rows.filter(r=>r.Status==='Released').length;
const planned = rows.filter(r=>r.Status==='Planned').length;
const overdue = rows.filter(r=>{
if(!r.DueDate) return false;
const [d,m,y]=r.DueDate.split('-'); return new Date(`${y}-${m}-${d}`) < today;
}).length;
const tp = rows.reduce((s,r)=>s+(parseFloat(r.PlannedQuantity)||0),0);
const tc = rows.reduce((s,r)=>s+(parseFloat(r.CompletedQty)||0),0);
const pct = tp>0 ? ((tc/tp)*100).toFixed(1) : '0.0';
showChips([
{label:'Total Orders', value:rows.length, cls:'blue'},
{label:'Released', value:released, cls:'green'},
{label:'Planned', value:planned},
{label:'Overdue', value:overdue, cls:'amber'},
{label:'Completion', value:pct+'%', cls:'red'},
]);
const cols = [
{k:'DocNum', l:'Doc No'},
{k:'SeriesName', l:'Series'},
{k:'Type', l:'Type'},
{k:'Status', l:'Status', render:(v)=>{
const bc=v==='Released'?'badge-released':'badge-planned';
return `<td><span class="badge ${bc}">${v}</span></td>`;
}},
{k:'ProductCode', l:'Product Code'},
{k:'ProductSegment', l:'Segment'},
{k:'ProductName', l:'Product Name', css:'trunc'},
{k:'PlannedQuantity', l:'Planned Qty', css:'num'},
{k:'CompletedQty', l:'Completed', css:'num'},
{k:'PostDate', l:'Post Date'},
{k:'StartDate', l:'Start Date'},
{k:'DueDate', l:'Due Date', render:(v,row)=>{
let cls='';
if(v){const [d,m,y]=v.split('-');if(new Date(`${y}-${m}-${d}`)<today)cls='over-due';}
return `<td class="${cls}">${v||''}</td>`;
}},
];
renderTable(cols, rows);
}
// ── FG INVENTORY ──────────────────────────────────────────────────────────
function renderInv(rows){
if (!rows.length){
document.getElementById('tblWrap').innerHTML = '<div class="empty">No inventory data found</div>';
return;
}
const totOp = rows.reduce((s,r)=>s+(parseFloat(r.OpeningBalanceQTY)||0),0);
const totPrd = rows.reduce((s,r)=>s+(parseFloat(r.ReceivedFromPrd)||0),0);
const totBal = rows.reduce((s,r)=>s+(parseFloat(r.Balance)||0),0);
const totLiv = rows.reduce((s,r)=>s+(parseFloat(r.QtyInWhse)||0),0);
showChips([
{label:'Items', value:rows.length, cls:'blue'},
{label:'Opening Qty', value:fmtN(totOp)},
{label:'Received (Prd)', value:fmtN(totPrd), cls:'green'},
{label:'Balance', value:fmtN(totBal), cls:'amber'},
{label:'Live Stock', value:fmtN(totLiv), cls:'purple'},
]);
const cols = [
{k:'Itemcode', l:'Item Code'},
{k:'ItemName', l:'Item Name', css:'trunc'},
{k:'ItemGroupName', l:'Group'},
{k:'OpeningBalanceQTY', l:'Opening Qty', css:'num'},
{k:'AvgRate1', l:'Avg Rate (Op)', css:'num'},
{k:'ReceivedFromPrd', l:'Recv. from Prod', css:'num'},
{k:'ReceivedStockQTY', l:'Stock Recv.', css:'num'},
{k:'AvgRate2', l:'Avg Rate (In)', css:'num'},
{k:'ARCreditMemoQty', l:'AR Credit Memo', css:'num'},
{k:'IssueStock', l:'Issue Stock', css:'num'},
{k:'AvgRate3', l:'Avg Rate (Out)', css:'num'},
{k:'StockTransferQty', l:'Stock Transfer', css:'num'},
{k:'Balance', l:'Balance', css:'num'},
{k:'QtyInWhse', l:'Live Stock', css:'num'},
];
renderTable(cols, rows);
}
// ── BOM COST ──────────────────────────────────────────────────────────────
function renderBom(rows, grp){
if (!rows.length){
document.getElementById('tblWrap').innerHTML = '<div class="empty">No BOM items found</div>';
return;
}
const minP = Math.min(...rows.map(r=>parseFloat(r.Price)||0).filter(v=>v>0));
const maxP = Math.max(...rows.map(r=>parseFloat(r.Price)||0));
const avgP = rows.reduce((s,r)=>s+(parseFloat(r.Price)||0),0)/rows.length;
showChips([
{label:'Total Items', value:rows.length, cls:'blue'},
{label:'Item Group', value:grp||'—'},
{label:'Min Price', value:fmtC(minP), cls:'green'},
{label:'Max Price', value:fmtC(maxP), cls:'red'},
{label:'Avg Price', value:fmtC(avgP), cls:'amber'},
]);
const cols = [
{k:'ItemCode', l:'Item Code'},
{k:'ItemName', l:'Item Name', css:'trunc'},
{k:'Price', l:'Price (₹)', css:'num'},
];
renderTable(cols, rows);
}
// ── SALES REPORT ──────────────────────────────────────────────────────────
function renderSales(rows){
if (!rows.length){
document.getElementById('tblWrap').innerHTML = '<div class="empty">No sales data found</div>';
return;
}
const yearly = rows.find(r=>(r.Period||'').toLowerCase().startsWith('yearly'));
showChips([
{label:'Blood Bag (TM)', value:fmtC(yearly?.TM_BloodBag || 0), cls:'blue'},
{label:'CAPD (PD)', value:fmtC(yearly?.PD_CAPD || 0), cls:'green'},
{label:'Export Direct', value:fmtC(yearly?.ExportDirect || 0), cls:'amber'},
{label:'Export Indirect', value:fmtC(yearly?.ExportIndirect || 0)},
{label:'Yearly Total', value:fmtC(yearly?.Total || 0), cls:'purple'},
]);
const cols = [
{k:'Period', l:'Period', css:'trunc'},
{k:'TM_BloodBag', l:'Blood Bag (TM)', css:'num'},
{k:'PD_CAPD', l:'CAPD (PD)', css:'num'},
{k:'ExportDirect', l:'Export Direct', css:'num'},
{k:'ExportIndirect', l:'Export Indirect', css:'num'},
{k:'Total', l:'Total', css:'num'},
];
renderTable(cols, rows);
}
// ── Export CSV ────────────────────────────────────────────────────────────
function exportCsv(){
if (!currentData.length) return;
const keys = Object.keys(currentData[0]);
const lines = [
keys.join(','),
...currentData.map(r=>keys.map(k=>{
const v = String(r[k]??'').replace(/"/g,'""');
return v.includes(',') ? `"${v}"` : v;
}).join(','))
];
const blob = new Blob([lines.join('\r\n')],{type:'text/csv'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `${currentReport}_report_${new Date().toISOString().split('T')[0]}.csv`;
a.click();
setStatus('CSV exported','ok');
}
window.loadData = loadData;
window.exportCsv = exportCsv;
window.switchReport = switchReport;
})();
</script>
<script src="/sidebar.js?v=25"></script>
</body>
</html>