1021 lines
49 KiB
HTML
1021 lines
49 KiB
HTML
<!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>Cost Sheet — Finance</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
|
||
<script>document.documentElement.setAttribute('data-theme',localStorage.getItem('portal_theme')||'obsidian')</script>
|
||
<link rel="stylesheet" href="/theme.css?v=3"/>
|
||
<style>
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{font-family:'Space Grotesk',sans-serif;background:var(--bg,#f8fafc);color:var(--text,#0f172a);-webkit-font-smoothing:antialiased}
|
||
|
||
/* ── Page wrapper ── */
|
||
.page{padding:20px 24px 32px}
|
||
|
||
/* ── Page header ── */
|
||
.page-hd{display:flex;align-items:flex-start;justify-content:space-between;flex-wrap:wrap;gap:12px;margin-bottom:20px}
|
||
.page-title{font-size:20px;font-weight:800;letter-spacing:-.4px;color:var(--text)}
|
||
.page-sub{font-size:12px;color:var(--text3);margin-top:3px}
|
||
|
||
/* ── Filter bar ── */
|
||
.filter-bar{
|
||
display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap;
|
||
background:var(--surface);border:1px solid var(--border);
|
||
border-radius:14px;padding:16px 20px;margin-bottom:20px;
|
||
}
|
||
.f-group{display:flex;flex-direction:column;gap:5px;min-width:140px}
|
||
.f-label{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:var(--text3)}
|
||
|
||
/* ── Searchable multi-select ── */
|
||
.sms-wrap{position:relative;width:100%}
|
||
.sms-box{border:1px solid var(--border);border-radius:8px;background:var(--surface);overflow:hidden}
|
||
.sms-search{display:flex;align-items:center;gap:6px;padding:5px 8px;border-bottom:1px solid var(--border);background:var(--surface)}
|
||
.sms-search input{flex:1;border:none;outline:none;font-size:12px;font-family:'Space Grotesk',sans-serif;background:transparent;color:var(--text)}
|
||
.sms-search input::placeholder{color:var(--text3)}
|
||
.sms-list{max-height:110px;overflow-y:auto}
|
||
.sms-list::-webkit-scrollbar{width:4px}
|
||
.sms-list::-webkit-scrollbar-thumb{background:#c1c7ce;border-radius:4px}
|
||
.sms-opt{display:flex;align-items:center;gap:8px;padding:5px 10px;cursor:pointer;font-size:12px;color:var(--text);transition:background .1s;user-select:none}
|
||
.sms-opt:hover{background:var(--surface3,#f1f5f9)}
|
||
.sms-opt.selected{background:#e0f4f4;color:#004d40}
|
||
.sms-opt input[type=checkbox]{width:13px;height:13px;accent-color:#047e7e;flex-shrink:0;cursor:pointer}
|
||
.sms-chips{display:flex;flex-wrap:wrap;gap:5px;padding:5px 8px;min-height:28px;border-bottom:1px solid var(--border);background:var(--surface3,#f8fafb)}
|
||
.sms-chip{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:5px;font-size:11px;font-weight:600;cursor:default}
|
||
.sms-chip.teal{background:#e0f4f4;color:#004d40;border:1px solid #80cbc4}
|
||
.sms-chip.purple{background:#ede9fe;color:#5b21b6;border:1px solid #c4b5fd}
|
||
.sms-chip button{background:none;border:none;cursor:pointer;font-size:13px;line-height:1;padding:0;color:inherit;opacity:.6}
|
||
.sms-chip button:hover{opacity:1}
|
||
.sms-empty{padding:10px;text-align:center;font-size:11px;color:var(--text3)}
|
||
.f-input,.f-select{
|
||
padding:8px 11px;border:1px solid var(--border);border-radius:8px;
|
||
background:var(--surface);color:var(--text);font-size:13px;
|
||
font-family:'Space Grotesk',sans-serif;outline:none;
|
||
transition:border-color .15s,box-shadow .15s;
|
||
}
|
||
.f-input:focus,.f-select:focus{border-color:var(--blue,#3b82f6);box-shadow:0 0 0 3px rgba(59,130,246,.1)}
|
||
.btn{
|
||
padding:9px 18px;border-radius:9px;border:none;
|
||
font-size:13px;font-weight:600;font-family:'Space Grotesk',sans-serif;
|
||
cursor:pointer;transition:all .15s;
|
||
}
|
||
.btn-primary{background:var(--blue,#3b82f6);color:#fff;box-shadow:0 2px 8px rgba(59,130,246,.25)}
|
||
.btn-primary:hover{filter:brightness(1.08)}
|
||
.btn-primary:disabled{opacity:.5;cursor:not-allowed}
|
||
.btn-outline{background:var(--surface);border:1px solid var(--border);color:var(--text2)}
|
||
.btn-outline:hover{border-color:var(--blue,#3b82f6);color:var(--blue,#3b82f6)}
|
||
|
||
/* ── Status bar ── */
|
||
.status-bar{
|
||
display:flex;align-items:center;gap:8px;
|
||
font-size:12px;font-weight:500;color:var(--text3);
|
||
margin-bottom:16px;padding:0 2px;
|
||
}
|
||
.status-dot{width:8px;height:8px;border-radius:50%;background:var(--text3);flex-shrink:0}
|
||
.status-dot.ok{background:#16a34a}
|
||
.status-dot.loading{background:#f59e0b;animation:pulse 1s infinite}
|
||
.status-dot.err{background:#dc2626}
|
||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
||
|
||
/* ── Cost sheet table ── */
|
||
.cs-wrap{background:var(--surface);border:1px solid var(--border);border-radius:14px;overflow:hidden;margin-bottom:16px}
|
||
.cs-tbl-scroll{overflow-x:auto;overflow-y:auto;max-height:52vh}
|
||
.cs-section-hd{
|
||
display:flex;align-items:center;gap:10px;
|
||
padding:10px 16px;
|
||
background:var(--surface3,#f1f5f9);
|
||
border-bottom:1px solid var(--border);
|
||
}
|
||
.cs-section-tag{
|
||
padding:3px 10px;border-radius:20px;
|
||
font-size:10px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;
|
||
}
|
||
.tag-bb{background:rgba(37,99,235,.1);color:#2563eb}
|
||
.tag-capd{background:rgba(124,58,237,.1);color:#7c3aed}
|
||
.cs-section-label{font-size:12px;font-weight:700;color:var(--text2)}
|
||
|
||
table.cs-tbl{width:100%;border-collapse:collapse;font-size:12.5px}
|
||
table.cs-tbl th{
|
||
padding:9px 12px;text-align:left;
|
||
background:var(--surface3,#f1f5f9);
|
||
color:var(--text3);font-size:10.5px;font-weight:700;
|
||
text-transform:uppercase;letter-spacing:.6px;
|
||
border-bottom:1px solid var(--border);white-space:nowrap;
|
||
position:sticky;top:0;z-index:2;
|
||
}
|
||
table.cs-tbl th.num,table.cs-tbl td.num{text-align:right}
|
||
table.cs-tbl td{
|
||
padding:8px 12px;border-bottom:1px solid var(--border);
|
||
color:var(--text);vertical-align:middle;
|
||
}
|
||
table.cs-tbl tbody tr:last-child td{border-bottom:none}
|
||
table.cs-tbl tbody tr:hover td{background:rgba(59,130,246,.03)}
|
||
.td-code{font-family:monospace;font-size:11.5px;color:var(--blue,#3b82f6);font-weight:600}
|
||
.td-desc{color:var(--text);font-weight:500;max-width:280px}
|
||
|
||
/* Total rows */
|
||
tr.total-row td{
|
||
background:var(--surface3,#f1f5f9) !important;
|
||
font-weight:700;border-top:2px solid var(--border);
|
||
font-size:12px;
|
||
}
|
||
tr.grand-total td{
|
||
background:linear-gradient(135deg,#1e3264,#2563eb) !important;
|
||
color:#fff !important;font-weight:700;font-size:13px;
|
||
}
|
||
tr.grand-total td.num{color:#fff !important}
|
||
|
||
/* ── Empty / loading states ── */
|
||
.empty{padding:40px;text-align:center;color:var(--text3);font-size:13px}
|
||
.skeleton-row td{background:linear-gradient(90deg,var(--surface3,#f1f5f9) 25%,var(--surface2,#f8fafc) 50%,var(--surface3,#f1f5f9) 75%);animation:shimmer 1.2s infinite}
|
||
@keyframes shimmer{0%{background-position:-400px 0}100%{background-position:400px 0}}
|
||
|
||
/* ── Configure Allocation Panel ── */
|
||
.cfg-panel{background:var(--surface);border:1px solid var(--border);border-radius:14px;margin-bottom:16px;overflow:hidden}
|
||
.cfg-hdr{display:flex;align-items:center;gap:10px;padding:11px 18px;cursor:pointer;user-select:none;border-bottom:1px solid transparent;transition:border-color .15s}
|
||
.cfg-hdr:hover{background:var(--surface3,#f8fafc)}
|
||
.cfg-hdr.open{border-bottom-color:var(--border)}
|
||
.cfg-title{font-size:12px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.5px}
|
||
.cfg-chev{margin-left:auto;font-size:13px;color:var(--text3);transition:transform .2s}
|
||
.cfg-hdr.open .cfg-chev{transform:rotate(180deg)}
|
||
.cfg-body{padding:14px 18px;display:none}
|
||
.cfg-body.open{display:block}
|
||
.cfg-table{width:100%;border-collapse:collapse;font-size:12px}
|
||
.cfg-table th{padding:6px 10px;text-align:left;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--text3);border-bottom:1px solid var(--border)}
|
||
.cfg-table th.r{text-align:right}
|
||
.cfg-table td{padding:7px 10px;border-bottom:1px solid var(--border);color:var(--text);vertical-align:middle}
|
||
.cfg-table tbody tr:last-child td{border-bottom:none}
|
||
.cfg-table td.r{text-align:right}
|
||
.cfg-inp{width:80px;padding:4px 8px;border:1px solid var(--border);border-radius:6px;font-size:12px;font-family:'Space Grotesk',sans-serif;background:var(--surface);color:var(--text);outline:none;text-align:right}
|
||
.cfg-inp:focus{border-color:var(--blue,#3b82f6);box-shadow:0 0 0 2px rgba(59,130,246,.1)}
|
||
.cfg-auto{font-size:11px;color:var(--text3);font-style:italic}
|
||
.cfg-badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:700}
|
||
.cfg-badge.bb{background:rgba(37,99,235,.1);color:#2563eb}
|
||
.cfg-badge.capd{background:rgba(124,58,237,.1);color:#7c3aed}
|
||
.cfg-reset{background:none;border:none;cursor:pointer;font-size:10px;color:var(--text3);padding:2px 6px;border-radius:4px;font-family:inherit}
|
||
.cfg-reset:hover{color:#dc2626;background:rgba(220,38,38,.06)}
|
||
.cfg-result{margin-top:10px;padding:8px 12px;background:var(--surface3,#f8fafc);border-radius:8px;font-size:11px;color:var(--text3);display:none}
|
||
.cfg-result.show{display:block}
|
||
.cfg-result b{color:var(--text2)}
|
||
|
||
/* ── Toast ── */
|
||
.toast-c{position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px;pointer-events:none}
|
||
.toast{padding:11px 18px;border-radius:9px;font-size:12.5px;font-weight:600;box-shadow:0 4px 16px rgba(0,0,0,.15);display:flex;align-items:center;gap:8px;pointer-events:auto;animation:toastIn .2s ease}
|
||
.toast.ok{background:#16a34a;color:#fff}
|
||
.toast.err{background:#dc2626;color:#fff}
|
||
@keyframes toastIn{from{transform:translateY(10px);opacity:0}to{transform:translateY(0);opacity:1}}
|
||
|
||
@media(max-width:768px){
|
||
.page{padding:12px 14px}
|
||
.filter-bar{padding:12px}
|
||
table.cs-tbl th,table.cs-tbl td{padding:6px 8px;font-size:11px}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="toast-c" id="toastC"></div>
|
||
<div class="page" id="pageRoot">
|
||
|
||
<!-- Page Header -->
|
||
<div class="page-hd">
|
||
<div>
|
||
<div class="page-title">Cost Sheet</div>
|
||
<div class="page-sub">Production · Sales · RM Consumption · Employee Cost</div>
|
||
</div>
|
||
<div style="display:flex;gap:8px">
|
||
<button class="btn btn-outline" id="btnPrint" disabled onclick="printCostSheet()">Print</button>
|
||
<button class="btn btn-outline" id="btnExport" disabled onclick="exportXLSX()">↓ Export Excel</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 1: dates + actions -->
|
||
<div class="filter-bar" style="border-bottom:none;border-radius:14px 14px 0 0;padding-bottom:10px">
|
||
<div class="f-group">
|
||
<label class="f-label">From Date</label>
|
||
<input type="date" class="f-input" id="fromDate"/>
|
||
</div>
|
||
<div class="f-group">
|
||
<label class="f-label">To Date</label>
|
||
<input type="date" class="f-input" id="toDate"/>
|
||
</div>
|
||
<div style="margin-left:auto;display:flex;gap:8px;align-items:flex-end">
|
||
<button class="btn btn-primary" id="btnLoad" onclick="loadData()">Load Cost Sheet</button>
|
||
<button class="btn btn-outline" onclick="clearAll()" style="color:#dc2626;border-color:#fca5a5">Clear All</button>
|
||
</div>
|
||
</div>
|
||
<!-- Row 2: salary + snapshot searchable multi-selects -->
|
||
<div style="display:flex;gap:14px;padding:10px 20px 14px;background:var(--surface);border:1px solid var(--border);border-top:none;border-radius:0 0 14px 14px;margin-bottom:20px;flex-wrap:wrap">
|
||
<div class="f-group" style="flex:1;min-width:200px">
|
||
<label class="f-label">Salary Periods</label>
|
||
<div class="sms-box" id="sms-sal">
|
||
<div class="sms-chips" id="sms-sal-chips"></div>
|
||
<div class="sms-search">
|
||
<input type="text" id="sms-sal-q" placeholder="Search periods…" oninput="smsFilter('sal')">
|
||
</div>
|
||
<div class="sms-list" id="sms-sal-list"></div>
|
||
</div>
|
||
</div>
|
||
<div class="f-group" style="flex:2;min-width:260px">
|
||
<label class="f-label">Employee Pool Snapshot <span style="font-weight:400;color:#9ca3af;text-transform:none">(blank = live data)</span></label>
|
||
<div class="sms-box" id="sms-snap">
|
||
<div class="sms-chips" id="sms-snap-chips"></div>
|
||
<div class="sms-search">
|
||
<input type="text" id="sms-snap-q" placeholder="Search snapshots…" oninput="smsFilter('snap')">
|
||
</div>
|
||
<div class="sms-list" id="sms-snap-list"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Boiler cost info bar (auto-computed, shown after load) -->
|
||
<div style="padding:6px 20px 10px;background:var(--surface);border:1px solid var(--border);border-top:none;border-radius:0 0 14px 14px;margin-bottom:12px">
|
||
<div id="boilerInfoBar" style="display:none;font-size:12px;color:#047e7e;font-weight:600;line-height:1.7"></div>
|
||
</div>
|
||
|
||
<!-- Employee pool label (shown after snapshot selection) -->
|
||
<div id="snapEmpPoolWrap" style="display:none;padding:6px 20px;background:#f5f0ff;border-radius:8px;margin-bottom:10px">
|
||
<span id="snapEmpPoolLabel" style="font-size:12px;color:#7c3aed;font-weight:700"></span>
|
||
</div>
|
||
|
||
<!-- Configure Allocation Panel -->
|
||
<div class="cfg-panel">
|
||
<div class="cfg-hdr" id="cfgHdr" onclick="toggleCfg()">
|
||
<span style="font-size:15px">⚙</span>
|
||
<span class="cfg-title">Configure Cost Allocation</span>
|
||
<span style="font-size:11px;color:var(--text3);font-weight:400">Blood Bag % vs CAPD % per cost head (blank = auto)</span>
|
||
<span class="cfg-chev">▼</span>
|
||
</div>
|
||
<div class="cfg-body" id="cfgBody">
|
||
<table class="cfg-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="min-width:130px">Cost Head</th>
|
||
<th class="r" style="min-width:140px">Total Pool (₹)</th>
|
||
<th class="r" style="min-width:90px;color:#0ea5e9">Power %</th>
|
||
<th class="r" style="min-width:120px;color:#0ea5e9">Power Absorbed (₹)</th>
|
||
<th class="r">Blood Bag %</th>
|
||
<th class="r" style="min-width:130px">BB Amount (₹)</th>
|
||
<th class="r">CAPD %</th>
|
||
<th class="r" style="min-width:130px">CAPD Amount (₹)</th>
|
||
<th class="r">Auto Basis</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td><span class="cfg-badge bb">Employee</span></td>
|
||
<td class="r cfg-auto" id="cfg-emp-pool">—</td>
|
||
<td class="r"><input class="cfg-inp" id="cfg-emp-power-pct" type="number" min="0" max="100" step="0.1" placeholder="0" style="border-color:#0ea5e9" oninput="syncPowerPct()"></td>
|
||
<td class="r cfg-auto" id="cfg-emp-power-abs" style="color:#0ea5e9">—</td>
|
||
<td class="r"><input class="cfg-inp" id="cfg-emp-bb" type="number" min="0" max="100" step="0.1" placeholder="auto" oninput="syncCfg('emp')"></td>
|
||
<td class="r cfg-auto" id="cfg-emp-bb-amt">—</td>
|
||
<td class="r" id="cfg-emp-capd-disp" style="font-size:12px;color:var(--text3)">—</td>
|
||
<td class="r cfg-auto" id="cfg-emp-capd-amt">—</td>
|
||
<td class="r cfg-auto" id="cfg-emp-auto">salary ratio</td>
|
||
<td><button class="cfg-reset" onclick="resetCfg('emp')">Reset</button></td>
|
||
</tr>
|
||
<tr>
|
||
<td><span class="cfg-badge bb">QA Cost</span></td>
|
||
<td class="r cfg-auto" id="cfg-qa-pool">—</td>
|
||
<td class="r"><input class="cfg-inp" id="cfg-qa-power-pct" type="number" min="0" max="100" step="0.1" placeholder="0" style="border-color:#0ea5e9" oninput="syncPowerPct()"></td>
|
||
<td class="r cfg-auto" id="cfg-qa-power-abs" style="color:#0ea5e9">—</td>
|
||
<td class="r"><input class="cfg-inp" id="cfg-qa-bb" type="number" min="0" max="100" step="0.1" placeholder="auto" oninput="syncCfg('qa')"></td>
|
||
<td class="r cfg-auto" id="cfg-qa-bb-amt">—</td>
|
||
<td class="r" id="cfg-qa-capd-disp" style="font-size:12px;color:var(--text3)">—</td>
|
||
<td class="r cfg-auto" id="cfg-qa-capd-amt">—</td>
|
||
<td class="r cfg-auto" id="cfg-qa-auto">RM ratio</td>
|
||
<td><button class="cfg-reset" onclick="resetCfg('qa')">Reset</button></td>
|
||
</tr>
|
||
<tr>
|
||
<td><span class="cfg-badge bb">QC Cost</span></td>
|
||
<td class="r cfg-auto" id="cfg-qc-pool">—</td>
|
||
<td class="r"><input class="cfg-inp" id="cfg-qc-power-pct" type="number" min="0" max="100" step="0.1" placeholder="0" style="border-color:#0ea5e9" oninput="syncPowerPct()"></td>
|
||
<td class="r cfg-auto" id="cfg-qc-power-abs" style="color:#0ea5e9">—</td>
|
||
<td class="r"><input class="cfg-inp" id="cfg-qc-bb" type="number" min="0" max="100" step="0.1" placeholder="auto" oninput="syncCfg('qc')"></td>
|
||
<td class="r cfg-auto" id="cfg-qc-bb-amt">—</td>
|
||
<td class="r" id="cfg-qc-capd-disp" style="font-size:12px;color:var(--text3)">—</td>
|
||
<td class="r cfg-auto" id="cfg-qc-capd-amt">—</td>
|
||
<td class="r cfg-auto" id="cfg-qc-auto">RM ratio</td>
|
||
<td><button class="cfg-reset" onclick="resetCfg('qc')">Reset</button></td>
|
||
</tr>
|
||
<tr>
|
||
<td><span class="cfg-badge capd">Boiler</span></td>
|
||
<td class="r cfg-auto" id="cfg-boiler-pool">—</td>
|
||
<td class="r"><input class="cfg-inp" id="cfg-boiler-power-pct" type="number" min="0" max="100" step="0.1" placeholder="0" style="border-color:#0ea5e9" oninput="syncPowerPct()"></td>
|
||
<td class="r cfg-auto" id="cfg-boiler-power-abs" style="color:#0ea5e9">—</td>
|
||
<td class="r"><input class="cfg-inp" id="cfg-boiler-bb" type="number" min="0" max="100" step="0.1" placeholder="auto" oninput="syncCfg('boiler')"></td>
|
||
<td class="r cfg-auto" id="cfg-boiler-bb-amt">—</td>
|
||
<td class="r" id="cfg-boiler-capd-disp" style="font-size:12px;color:var(--text3)">—</td>
|
||
<td class="r cfg-auto" id="cfg-boiler-capd-amt">—</td>
|
||
<td class="r cfg-auto" id="cfg-boiler-auto">RM ratio</td>
|
||
<td><button class="cfg-reset" onclick="resetCfg('boiler')">Reset</button></td>
|
||
</tr>
|
||
<tr style="background:rgba(14,165,233,.04)">
|
||
<td><span class="cfg-badge capd">Power Cost</span></td>
|
||
<td class="r cfg-auto" id="cfg-power-pool">—</td>
|
||
<td class="r cfg-auto" id="cfg-power-remaining" style="color:#0ea5e9;font-weight:700">remaining</td>
|
||
<td class="r cfg-auto" id="cfg-power-power-abs" style="color:#0ea5e9">—</td>
|
||
<td class="r"><input class="cfg-inp" id="cfg-power-bb" type="number" min="0" max="100" step="0.1" placeholder="auto" oninput="syncCfg('power')"></td>
|
||
<td class="r cfg-auto" id="cfg-power-bb-amt">—</td>
|
||
<td class="r" id="cfg-power-capd-disp" style="font-size:12px;color:var(--text3)">—</td>
|
||
<td class="r cfg-auto" id="cfg-power-capd-amt">—</td>
|
||
<td class="r cfg-auto" id="cfg-power-auto">RM ratio</td>
|
||
<td><button class="cfg-reset" onclick="resetCfg('power')">Reset</button></td>
|
||
</tr>
|
||
<tr>
|
||
<td><span class="cfg-badge capd">R&M Cost</span></td>
|
||
<td class="r cfg-auto" id="cfg-repair-pool">—</td>
|
||
<td class="r"><input class="cfg-inp" id="cfg-repair-power-pct" type="number" min="0" max="100" step="0.1" placeholder="0" style="border-color:#0ea5e9" oninput="syncPowerPct()"></td>
|
||
<td class="r cfg-auto" id="cfg-repair-power-abs" style="color:#0ea5e9">—</td>
|
||
<td class="r"><input class="cfg-inp" id="cfg-repair-bb" type="number" min="0" max="100" step="0.1" placeholder="auto" oninput="syncCfg('repair')"></td>
|
||
<td class="r cfg-auto" id="cfg-repair-bb-amt">—</td>
|
||
<td class="r" id="cfg-repair-capd-disp" style="font-size:12px;color:var(--text3)">—</td>
|
||
<td class="r cfg-auto" id="cfg-repair-capd-amt">—</td>
|
||
<td class="r cfg-auto" id="cfg-repair-auto">RM ratio</td>
|
||
<td><button class="cfg-reset" onclick="resetCfg('repair')">Reset</button></td>
|
||
</tr>
|
||
<tr>
|
||
<td><span class="cfg-badge capd">R&D Cost</span></td>
|
||
<td class="r cfg-auto" id="cfg-rd-pool">—</td>
|
||
<td class="r"><input class="cfg-inp" id="cfg-rd-power-pct" type="number" min="0" max="100" step="0.1" placeholder="0" style="border-color:#0ea5e9" oninput="syncPowerPct()"></td>
|
||
<td class="r cfg-auto" id="cfg-rd-power-abs" style="color:#0ea5e9">—</td>
|
||
<td class="r"><input class="cfg-inp" id="cfg-rd-bb" type="number" min="0" max="100" step="0.1" placeholder="auto" oninput="syncCfg('rd')"></td>
|
||
<td class="r cfg-auto" id="cfg-rd-bb-amt">—</td>
|
||
<td class="r" id="cfg-rd-capd-disp" style="font-size:12px;color:var(--text3)">—</td>
|
||
<td class="r cfg-auto" id="cfg-rd-capd-amt">—</td>
|
||
<td class="r cfg-auto" id="cfg-rd-auto">RM ratio</td>
|
||
<td><button class="cfg-reset" onclick="resetCfg('rd')">Reset</button></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="cfg-result" id="cfgResult"></div>
|
||
<div style="padding:10px 16px 14px;display:flex;gap:8px;align-items:center">
|
||
<button class="btn btn-primary" onclick="loadData()" style="padding:8px 20px;font-size:12px">Apply & Reload</button>
|
||
<button class="btn btn-outline" onclick="resetAllCfg()" style="padding:8px 14px;font-size:12px;color:#dc2626;border-color:#fca5a5">Reset All</button>
|
||
<span style="font-size:11px;color:var(--text3)">Changes take effect after reload</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Status -->
|
||
<div class="status-bar" id="statusBar">
|
||
<span class="status-dot" id="statusDot"></span>
|
||
<span id="statusMsg">Select a date range and click Load.</span>
|
||
</div>
|
||
|
||
<!-- Content placeholder -->
|
||
<div id="csContent"></div>
|
||
|
||
</div>
|
||
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
|
||
<script>
|
||
'use strict';
|
||
const tok = sessionStorage.getItem('portal_token') || '';
|
||
const hdrs = { 'Content-Type':'application/json', Authorization: 'Bearer '+tok };
|
||
|
||
let pageData = null;
|
||
let _lastPowerPool = 0;
|
||
let snapEmpPool = null;
|
||
let snapBoilerPool = null;
|
||
let snapPowerPool = null;
|
||
let snapRepairPool = null;
|
||
let snapRdPool = null;
|
||
let boilerInfo = null;
|
||
|
||
// ── Load snapshot list ────────────────────────────────────────────────────────
|
||
(async function loadSnapshotList() {
|
||
try {
|
||
const r = await fetch('/api/costing/snapshots', { headers: hdrs }).then(r => r.json());
|
||
if (!r.success || !r.data.length) return;
|
||
smsState.snap.options = r.data.map(s => {
|
||
const dt = new Date(s.saved_at).toLocaleDateString('en-IN');
|
||
return { value: String(s.id), label: `${s.snap_name} (${s.period_from}→${s.period_to}) ${dt}` };
|
||
});
|
||
smsRenderList('snap'); smsRenderChips('snap');
|
||
} catch(_) {}
|
||
})();
|
||
|
||
const EXCLUDE_EMP_CODES = new Set([
|
||
'5103030001','5103030002','5103030003','5103030004','5103030005',
|
||
'5103030006','5103030008','5103030010','5103030012','5103030013',
|
||
'5240040001','5240040002','5240040003','5240040004','5240040005',
|
||
'5240040006','5240040008','5240040009','5240040010','5240040012',
|
||
'5240040013','5240040015'
|
||
]);
|
||
const MED_FEST_CODES = new Set(['5190090026','5190090029']);
|
||
|
||
function calcEmpPoolFromSnapshot(accounts) {
|
||
let pool = 0;
|
||
accounts.forEach(r => {
|
||
if (MED_FEST_CODES.has(r.acct_code)) {
|
||
pool += (r.period_debit || 0) + (r.provision || 0);
|
||
return;
|
||
}
|
||
if (r.sheet === 'Employee' && !EXCLUDE_EMP_CODES.has(r.acct_code) &&
|
||
!(r.acct_name || '').toUpperCase().includes('S&M')) {
|
||
pool += (r.period_debit - r.period_credit) + (r.provision || 0);
|
||
}
|
||
});
|
||
return pool;
|
||
}
|
||
|
||
function calcBoilerFromSnapshot(accounts) {
|
||
const row = accounts.find(a => a.acct_code === '5110010010');
|
||
if (!row) return null;
|
||
return Number(row.period_debit||0) - Number(row.period_credit||0) + (row.provision!=null?Number(row.provision):0);
|
||
}
|
||
|
||
function calcPowerFromSnapshot(accounts) {
|
||
const rows = accounts.filter(a => a.acct_code==='5110010008' || a.acct_code==='5110010009');
|
||
if (!rows.length) return null;
|
||
return rows.reduce((sum, row) =>
|
||
sum + Number(row.period_debit||0) - Number(row.period_credit||0) + (row.provision!=null?Number(row.provision):0),
|
||
0);
|
||
}
|
||
|
||
function calcRepairFromSnapshot(accounts) {
|
||
const RM_ACCTS = new Set(['5190090010','5190090011','5190090012','5190090013','5190090028']);
|
||
const rows = accounts.filter(a => RM_ACCTS.has(a.acct_code));
|
||
if (!rows.length) return null;
|
||
return rows.reduce((sum, row) =>
|
||
sum + Number(row.period_debit||0) - Number(row.period_credit||0) + (row.provision!=null?Number(row.provision):0),
|
||
0);
|
||
}
|
||
|
||
function calcRdFromSnapshot(accounts) {
|
||
const RD_ACCTS = new Set(['5240040018','5240040019','5240040020','5240040021']);
|
||
const rows = accounts.filter(a => RD_ACCTS.has(a.acct_code));
|
||
if (!rows.length) return null;
|
||
return rows.reduce((sum, row) =>
|
||
sum + Number(row.period_debit||0) - Number(row.period_credit||0) + (row.provision!=null?Number(row.provision):0),
|
||
0);
|
||
}
|
||
|
||
async function onSnapSelect() {
|
||
const lbl = document.getElementById('snapEmpPoolLabel');
|
||
const wrap = document.getElementById('snapEmpPoolWrap');
|
||
const ids = smsState.snap.selected;
|
||
|
||
if (!ids.length) {
|
||
snapEmpPool = null; snapBoilerPool = null; snapPowerPool = null; snapRepairPool = null; snapRdPool = null;
|
||
if (wrap) wrap.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
if (lbl) lbl.textContent = 'Calculating…';
|
||
if (wrap) wrap.style.display = '';
|
||
try {
|
||
let totalEmp = 0, totalBoiler = 0, totalPower = 0, totalRepair = 0, totalRd = 0;
|
||
let boilerFound = false, powerFound = false, repairFound = false, rdFound = false;
|
||
for (const id of ids) {
|
||
const r = await fetch(`/api/costing/snapshot/${id}`, { headers: hdrs }).then(r => r.json());
|
||
if (!r.success) throw new Error(r.message);
|
||
const accounts = (r.data.accounts || []).map(a => ({
|
||
...a, provision: a.override_val != null ? Number(a.override_val) : null
|
||
}));
|
||
totalEmp += calcEmpPoolFromSnapshot(accounts);
|
||
const boiler = calcBoilerFromSnapshot(accounts);
|
||
if (boiler !== null) { totalBoiler += boiler; boilerFound = true; }
|
||
const power = calcPowerFromSnapshot(accounts);
|
||
if (power !== null) { totalPower += power; powerFound = true; }
|
||
const repair = calcRepairFromSnapshot(accounts);
|
||
if (repair !== null) { totalRepair += repair; repairFound = true; }
|
||
const rd = calcRdFromSnapshot(accounts);
|
||
if (rd !== null) { totalRd += rd; rdFound = true; }
|
||
}
|
||
snapEmpPool = totalEmp;
|
||
snapBoilerPool = boilerFound ? totalBoiler : null;
|
||
snapPowerPool = powerFound ? totalPower : null;
|
||
snapRepairPool = repairFound ? totalRepair : null;
|
||
snapRdPool = rdFound ? totalRd : null;
|
||
if (lbl) {
|
||
const fmtI = v => v.toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2});
|
||
const extra = [
|
||
boilerFound ? `Boiler: ₹ ${fmtI(totalBoiler)}` : '',
|
||
powerFound ? `Power: ₹ ${fmtI(totalPower)}` : '',
|
||
repairFound ? `R&M: ₹ ${fmtI(totalRepair)}` : '',
|
||
rdFound ? `R&D: ₹ ${fmtI(totalRd)}` : '',
|
||
].filter(Boolean).join(' · ');
|
||
lbl.textContent = `Employee Pool: ₹ ${fmtI(totalEmp)}` + (extra ? ` · ${extra}` : '');
|
||
}
|
||
} catch(e) {
|
||
snapEmpPool = null; snapBoilerPool = null; snapPowerPool = null; snapRepairPool = null; snapRdPool = null;
|
||
if (lbl) lbl.textContent = 'Error: ' + e.message;
|
||
}
|
||
}
|
||
|
||
function toast(msg, type='ok') {
|
||
const el = document.createElement('div');
|
||
el.className = 'toast ' + type;
|
||
el.textContent = msg;
|
||
document.getElementById('toastC').appendChild(el);
|
||
setTimeout(() => el.remove(), 4000);
|
||
}
|
||
|
||
function setStatus(msg, state='') {
|
||
document.getElementById('statusMsg').textContent = msg;
|
||
const dot = document.getElementById('statusDot');
|
||
dot.className = 'status-dot' + (state ? ' ' + state : '');
|
||
}
|
||
|
||
function fmt(v) {
|
||
if (v === null || v === undefined || v === '') return '—';
|
||
const n = parseFloat(v);
|
||
if (isNaN(n)) return v;
|
||
return n.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
}
|
||
function fmtQty(v) {
|
||
const n = parseFloat(v) || 0;
|
||
return n.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
}
|
||
|
||
// Default dates to current month
|
||
(function initDates() {
|
||
const now = new Date();
|
||
const y = now.getFullYear(), m = String(now.getMonth()+1).padStart(2,'0');
|
||
document.getElementById('fromDate').value = `${y}-${m}-01`;
|
||
const last = new Date(y, now.getMonth()+1, 0);
|
||
document.getElementById('toDate').value = `${y}-${m}-${String(last.getDate()).padStart(2,'0')}`;
|
||
})();
|
||
|
||
// ── Searchable multi-select engine ───────────────────────────────────────────
|
||
const smsState = {
|
||
sal: { options:[], selected:[] },
|
||
snap: { options:[], selected:[] },
|
||
};
|
||
|
||
function smsRenderList(key) {
|
||
const q = (document.getElementById(`sms-${key}-q`)?.value || '').toLowerCase();
|
||
const list = document.getElementById(`sms-${key}-list`);
|
||
const opts = smsState[key].options.filter(o => !q || o.label.toLowerCase().includes(q));
|
||
if (!opts.length) { list.innerHTML=`<div class="sms-empty">No results</div>`; return; }
|
||
list.innerHTML = opts.map(o => {
|
||
const sel = smsState[key].selected.includes(o.value);
|
||
return `<label class="sms-opt${sel?' selected':''}">
|
||
<input type="checkbox" ${sel?'checked':''} onchange="smsToggle('${key}','${o.value.replace(/'/g,"\\'")}',this.checked)">
|
||
<span>${o.label}</span>
|
||
</label>`;
|
||
}).join('');
|
||
}
|
||
|
||
function smsRenderChips(key) {
|
||
const el = document.getElementById(`sms-${key}-chips`);
|
||
if (!el) return;
|
||
const isTeal = key === 'sal';
|
||
el.innerHTML = smsState[key].selected.map(v => {
|
||
const opt = smsState[key].options.find(o => o.value === v);
|
||
return `<span class="sms-chip ${isTeal?'teal':'purple'}">${opt?opt.label:v}
|
||
<button onclick="smsToggle('${key}','${v.replace(/'/g,"\\'")}',false)">×</button>
|
||
</span>`;
|
||
}).join('') || `<span style="font-size:11px;color:var(--text3)">None selected</span>`;
|
||
}
|
||
|
||
function smsToggle(key, value, checked) {
|
||
const s = smsState[key].selected;
|
||
if (checked && !s.includes(value)) s.push(value);
|
||
if (!checked) { const i=s.indexOf(value); if(i>=0) s.splice(i,1); }
|
||
smsRenderChips(key);
|
||
smsRenderList(key);
|
||
if (key === 'snap') onSnapSelect();
|
||
}
|
||
window.smsToggle = smsToggle;
|
||
window.smsFilter = key => smsRenderList(key);
|
||
|
||
// ── Configure Allocation Panel ────────────────────────────────────────────────
|
||
function toggleCfg() {
|
||
const hdr = document.getElementById('cfgHdr');
|
||
const body = document.getElementById('cfgBody');
|
||
hdr.classList.toggle('open');
|
||
body.classList.toggle('open');
|
||
}
|
||
|
||
function syncCfg(key) {
|
||
const bbEl = document.getElementById(`cfg-${key}-bb`);
|
||
const capdEl = document.getElementById(`cfg-${key}-capd-disp`);
|
||
const val = bbEl.value.trim();
|
||
if (val === '' || isNaN(parseFloat(val))) {
|
||
capdEl.textContent = '—';
|
||
capdEl.style.color = 'var(--text3)';
|
||
} else {
|
||
const bb = Math.min(100, Math.max(0, parseFloat(val)));
|
||
capdEl.textContent = (100 - bb).toFixed(1) + '%';
|
||
capdEl.style.color = 'var(--text)';
|
||
}
|
||
}
|
||
|
||
function resetCfg(key) {
|
||
const bbEl = document.getElementById(`cfg-${key}-bb`);
|
||
const capdEl = document.getElementById(`cfg-${key}-capd-disp`);
|
||
if (bbEl) { bbEl.value = ''; }
|
||
if (capdEl) { capdEl.textContent = '—'; capdEl.style.color = 'var(--text3)'; }
|
||
const pwrEl = document.getElementById(`cfg-${key}-power-pct`);
|
||
const absEl = document.getElementById(`cfg-${key}-power-abs`);
|
||
if (pwrEl) pwrEl.value = '';
|
||
if (absEl) absEl.textContent = '—';
|
||
syncPowerPct();
|
||
}
|
||
|
||
function resetAllCfg() {
|
||
['emp','qa','qc','boiler','power','repair','rd'].forEach(k => resetCfg(k));
|
||
if (pageData) loadData();
|
||
}
|
||
window.resetAllCfg = resetAllCfg;
|
||
|
||
// Live preview: show power absorbed per cost head as user types
|
||
function syncPowerPct() {
|
||
const powerPool = _lastPowerPool || 0;
|
||
const distKeys = ['emp','qa','qc','boiler','repair','rd'];
|
||
let totalPct = 0;
|
||
distKeys.forEach(k => {
|
||
const v = parseFloat(document.getElementById(`cfg-${k}-power-pct`)?.value || 0) || 0;
|
||
totalPct += v;
|
||
const amt = powerPool * v / 100;
|
||
const el = document.getElementById(`cfg-${k}-power-abs`);
|
||
if (el) el.textContent = v > 0
|
||
? '₹' + amt.toLocaleString('en-IN', {minimumFractionDigits:2,maximumFractionDigits:2})
|
||
: '—';
|
||
});
|
||
const remaining = Math.max(0, powerPool * (1 - totalPct/100));
|
||
const remEl = document.getElementById('cfg-power-power-abs');
|
||
const remEl2 = document.getElementById('cfg-power-remaining');
|
||
if (remEl) remEl.textContent = '₹' + remaining.toLocaleString('en-IN',{minimumFractionDigits:2,maximumFractionDigits:2});
|
||
if (remEl2) remEl2.style.color = totalPct > 100 ? '#dc2626' : '#0ea5e9';
|
||
}
|
||
window.syncPowerPct = syncPowerPct;
|
||
|
||
function getCfgParams() {
|
||
const keys = ['emp','qa','qc','boiler','power','repair','rd'];
|
||
const apiKeys = { emp:'bbEmpPct', qa:'bbQaPct', qc:'bbQcPct', boiler:'bbBoilerPct', power:'bbPowerPct', repair:'bbRepairPct', rd:'bbRdPct' };
|
||
const pwrApiKeys = { emp:'empPowerPct', qa:'qaPowerPct', qc:'qcPowerPct', boiler:'boilerPowerPct', repair:'repairPowerPct', rd:'rdPowerPct' };
|
||
const parts = [];
|
||
keys.forEach(k => {
|
||
const v = document.getElementById(`cfg-${k}-bb`).value.trim();
|
||
if (v !== '' && !isNaN(parseFloat(v))) {
|
||
const clamped = Math.min(100, Math.max(0, parseFloat(v)));
|
||
parts.push(`${apiKeys[k]}=${clamped}`);
|
||
}
|
||
// Power distribution %
|
||
if (k !== 'power') {
|
||
const pv = document.getElementById(`cfg-${k}-power-pct`)?.value.trim() || '';
|
||
if (pv !== '' && !isNaN(parseFloat(pv))) {
|
||
const clamped = Math.min(100, Math.max(0, parseFloat(pv)));
|
||
if (clamped > 0) parts.push(`${pwrApiKeys[k]}=${clamped}`);
|
||
}
|
||
}
|
||
});
|
||
return parts.join('&');
|
||
}
|
||
|
||
function updateCfgAfterLoad(allocation) {
|
||
if (!allocation) return;
|
||
const fmtC = v => typeof v === 'number'
|
||
? '₹' + v.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||
: '—';
|
||
const autoBasis = { emp: 'salary ratio', qa: 'RM ratio', qc: 'RM ratio', boiler: 'RM ratio', power: 'RM ratio', repair: 'RM ratio', rd: 'RM ratio' };
|
||
const map = {
|
||
emp: allocation.emp, qa: allocation.qa, qc: allocation.qc,
|
||
boiler: allocation.boiler, power: allocation.power, repair: allocation.repair, rd: allocation.rd,
|
||
};
|
||
|
||
// Track total power pool for live preview
|
||
const pw = map.power;
|
||
if (pw) _lastPowerPool = (pw.bbPool || 0) + (pw.capdPool || 0) + (pw.powerAbsorbed || 0);
|
||
|
||
let summary = [];
|
||
['emp','qa','qc','boiler','power','repair','rd'].forEach(k => {
|
||
const d = map[k];
|
||
if (!d) return;
|
||
const bbPool = typeof d.bbPool === 'number' ? d.bbPool : 0;
|
||
const capdPool = typeof d.capdPool === 'number' ? d.capdPool : 0;
|
||
const total = bbPool + capdPool;
|
||
const bbPct = typeof d.bbPct === 'number' ? d.bbPct.toFixed(1) : '?';
|
||
const capdPct = (100 - parseFloat(bbPct)).toFixed(1);
|
||
|
||
const poolEl = document.getElementById(`cfg-${k}-pool`);
|
||
const bbAmtEl = document.getElementById(`cfg-${k}-bb-amt`);
|
||
const capdAmtEl = document.getElementById(`cfg-${k}-capd-amt`);
|
||
const autoEl = document.getElementById(`cfg-${k}-auto`);
|
||
const absEl = document.getElementById(`cfg-${k}-power-abs`);
|
||
|
||
if (poolEl) poolEl.textContent = fmtC(total);
|
||
if (bbAmtEl) bbAmtEl.textContent = fmtC(bbPool);
|
||
if (capdAmtEl) capdAmtEl.textContent = fmtC(capdPool);
|
||
if (absEl && typeof d.powerAbsorbed === 'number' && d.powerAbsorbed > 0)
|
||
absEl.textContent = fmtC(d.powerAbsorbed);
|
||
if (autoEl) {
|
||
const autoPct = typeof d.autoPct === 'number' ? d.autoPct.toFixed(1) : '?';
|
||
autoEl.textContent = `${autoBasis[k]} → ${autoPct}% BB`;
|
||
}
|
||
syncCfg(k);
|
||
summary.push(`<b>${k.toUpperCase()}</b>: BB ${bbPct}% / CAPD ${capdPct}%`);
|
||
});
|
||
syncPowerPct();
|
||
const res = document.getElementById('cfgResult');
|
||
if (res) {
|
||
res.innerHTML = 'Applied: ' + summary.join(' · ');
|
||
res.classList.add('show');
|
||
}
|
||
}
|
||
window.toggleCfg = toggleCfg;
|
||
window.syncCfg = syncCfg;
|
||
window.resetCfg = resetCfg;
|
||
|
||
function clearAll() {
|
||
smsState.sal.selected = [];
|
||
smsState.snap.selected = [];
|
||
smsRenderChips('sal'); smsRenderList('sal');
|
||
smsRenderChips('snap'); smsRenderList('snap');
|
||
snapEmpPool = null; snapBoilerPool = null; snapPowerPool = null; snapRepairPool = null; snapRdPool = null;
|
||
const lbl = document.getElementById('snapEmpPoolLabel');
|
||
if (lbl) { lbl.style.display='none'; lbl.textContent=''; }
|
||
}
|
||
window.clearAll = clearAll;
|
||
|
||
// Load salary periods
|
||
(async function loadPeriods() {
|
||
try {
|
||
const r = await fetch('/api/costsheet/salary-periods', { headers: hdrs }).then(r => r.json());
|
||
if (r.success && r.data.length) {
|
||
smsState.sal.options = r.data.map(p => ({ value: p, label: p }));
|
||
smsRenderList('sal'); smsRenderChips('sal');
|
||
}
|
||
} catch(_) {}
|
||
})();
|
||
|
||
async function loadData() {
|
||
const from = document.getElementById('fromDate').value;
|
||
const to = document.getElementById('toDate').value;
|
||
const sp = smsState.sal.selected.join(',');
|
||
if (!from || !to) { toast('Please select a date range.','err'); return; }
|
||
|
||
const btn = document.getElementById('btnLoad');
|
||
btn.disabled = true;
|
||
setStatus('Loading cost sheet…', 'loading');
|
||
document.getElementById('csContent').innerHTML = '';
|
||
document.getElementById('btnExport').disabled = true;
|
||
|
||
try {
|
||
const cfgParams = getCfgParams();
|
||
const url = `/api/costsheet/data?from=${from}&to=${to}${sp?'&salaryPeriod='+sp:''}${snapEmpPool!=null?'&overrideEmpPool='+snapEmpPool:''}${snapBoilerPool!=null?'&overrideBoilerLedger='+snapBoilerPool:''}${snapPowerPool!=null?'&overridePowerLedger='+snapPowerPool:''}${snapRepairPool!=null?'&overrideRepairLedger='+snapRepairPool:''}${snapRdPool!=null?'&overrideRdLedger='+snapRdPool:''}${cfgParams?'&'+cfgParams:''}`;
|
||
const r = await fetch(url, { headers: hdrs }).then(r => r.json());
|
||
if (!r.success) throw new Error(r.message);
|
||
|
||
pageData = r.data;
|
||
boilerInfo = r.boilerInfo || null;
|
||
updateCfgAfterLoad(r.allocation);
|
||
|
||
// Show boiler breakdown
|
||
const bar = document.getElementById('boilerInfoBar');
|
||
if (bar && boilerInfo) {
|
||
bar.style.display = '';
|
||
bar.innerHTML = `Boiler Ledger (5110010010): <b>₹${fmt(boilerInfo.ledger)}</b> + Boiler Salary: <b>₹${fmt(boilerInfo.salary)}</b> = Total Boiler: <b style="color:#047e7e">₹${fmt(boilerInfo.total)}</b>`;
|
||
}
|
||
|
||
renderSheet(pageData, from, to);
|
||
document.getElementById('btnExport').disabled = false;
|
||
document.getElementById('btnPrint').disabled = false;
|
||
const bbCount = pageData.bloodbag.length, capdCount = pageData.capd.length;
|
||
setStatus(`Loaded: ${bbCount} Blood Bag items · ${capdCount} CAPD items (${from} to ${to})`, 'ok');
|
||
} catch(e) {
|
||
setStatus(e.message, 'err');
|
||
toast(e.message, 'err');
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
function sectionTotal(rows) {
|
||
return rows.reduce((t, r) => ({
|
||
productionQty: t.productionQty + r.productionQty,
|
||
salesQty: t.salesQty + r.salesQty,
|
||
rmConsumption: t.rmConsumption + r.rmConsumption,
|
||
employeeCost: t.employeeCost + r.employeeCost,
|
||
qaCost: t.qaCost + (r.qaCost || 0),
|
||
qcCost: t.qcCost + (r.qcCost || 0),
|
||
boilerCost: t.boilerCost + (r.boilerCost || 0),
|
||
powerCost: t.powerCost + (r.powerCost || 0),
|
||
repairCost: t.repairCost + (r.repairCost || 0),
|
||
rdCost: t.rdCost + (r.rdCost || 0),
|
||
}), { productionQty:0, salesQty:0, rmConsumption:0, employeeCost:0, qaCost:0, qcCost:0, boilerCost:0, powerCost:0, repairCost:0, rdCost:0 });
|
||
}
|
||
|
||
function buildTableRows(rows) {
|
||
if (!rows.length) return `<tr><td colspan="12" class="empty">No data for this period</td></tr>`;
|
||
return rows.map(r => `
|
||
<tr>
|
||
<td class="td-code">${r.itemCode}</td>
|
||
<td class="td-desc">${r.itemDescription || '—'}</td>
|
||
<td class="num">${fmtQty(r.productionQty)}</td>
|
||
<td class="num">${fmtQty(r.salesQty)}</td>
|
||
<td class="num">${fmt(r.rmConsumption)}</td>
|
||
<td class="num">${fmt(r.employeeCost)}</td>
|
||
<td class="num">${fmt(r.qaCost)}</td>
|
||
<td class="num">${fmt(r.qcCost)}</td>
|
||
<td class="num">${fmt(r.boilerCost)}</td>
|
||
<td class="num">${fmt(r.powerCost)}</td>
|
||
<td class="num">${fmt(r.repairCost)}</td>
|
||
<td class="num">${fmt(r.rdCost)}</td>
|
||
</tr>`).join('');
|
||
}
|
||
|
||
function buildTotalRow(label, t) {
|
||
return `<tr class="total-row">
|
||
<td colspan="2" style="letter-spacing:.5px">${label}</td>
|
||
<td class="num">${fmtQty(t.productionQty)}</td>
|
||
<td class="num">${fmtQty(t.salesQty)}</td>
|
||
<td class="num">${fmt(t.rmConsumption)}</td>
|
||
<td class="num">${fmt(t.employeeCost)}</td>
|
||
<td class="num">${fmt(t.qaCost)}</td>
|
||
<td class="num">${fmt(t.qcCost)}</td>
|
||
<td class="num">${fmt(t.boilerCost)}</td>
|
||
<td class="num">${fmt(t.powerCost)}</td>
|
||
<td class="num">${fmt(t.repairCost)}</td>
|
||
<td class="num">${fmt(t.rdCost)}</td>
|
||
</tr>`;
|
||
}
|
||
|
||
function renderSheet(data, from, to) {
|
||
const bbTot = sectionTotal(data.bloodbag);
|
||
const capdTot = sectionTotal(data.capd);
|
||
const grand = {
|
||
productionQty: bbTot.productionQty + capdTot.productionQty,
|
||
salesQty: bbTot.salesQty + capdTot.salesQty,
|
||
rmConsumption: bbTot.rmConsumption + capdTot.rmConsumption,
|
||
employeeCost: bbTot.employeeCost + capdTot.employeeCost,
|
||
qaCost: bbTot.qaCost + capdTot.qaCost,
|
||
qcCost: bbTot.qcCost + capdTot.qcCost,
|
||
boilerCost: bbTot.boilerCost + capdTot.boilerCost,
|
||
powerCost: bbTot.powerCost + capdTot.powerCost,
|
||
repairCost: bbTot.repairCost + capdTot.repairCost,
|
||
rdCost: bbTot.rdCost + capdTot.rdCost,
|
||
};
|
||
|
||
const header = `
|
||
<thead><tr>
|
||
<th style="min-width:100px">Item Code</th>
|
||
<th style="min-width:220px">Item Description</th>
|
||
<th class="num" style="min-width:130px">Production (Units)</th>
|
||
<th class="num" style="min-width:110px">Sales Qty</th>
|
||
<th class="num" style="min-width:140px">RM Consumption</th>
|
||
<th class="num" style="min-width:130px">Employee Cost</th>
|
||
<th class="num" style="min-width:110px">QA Cost</th>
|
||
<th class="num" style="min-width:110px">QC Cost</th>
|
||
<th class="num" style="min-width:120px">Boiler Cost</th>
|
||
<th class="num" style="min-width:120px">Power Cost</th>
|
||
<th class="num" style="min-width:120px">R&M Cost</th>
|
||
<th class="num" style="min-width:120px">R&D Cost</th>
|
||
</tr></thead>`;
|
||
|
||
document.getElementById('csContent').innerHTML = `
|
||
<!-- BLOOD BAG Section -->
|
||
<div class="cs-wrap">
|
||
<div class="cs-section-hd">
|
||
<span class="cs-section-tag tag-bb">Blood Bag</span>
|
||
<span class="cs-section-label">FG BLOOD BAG · ${from} to ${to}</span>
|
||
</div>
|
||
<div class="cs-tbl-scroll">
|
||
<table class="cs-tbl">
|
||
${header}
|
||
<tbody>
|
||
${buildTableRows(data.bloodbag)}
|
||
${buildTotalRow('BLOOD BAG TOTAL', bbTot)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CAPD Section -->
|
||
<div class="cs-wrap">
|
||
<div class="cs-section-hd">
|
||
<span class="cs-section-tag tag-capd">CAPD</span>
|
||
<span class="cs-section-label">FG CAPD / FG CAPD ACCESSORIES · ${from} to ${to}</span>
|
||
</div>
|
||
<div class="cs-tbl-scroll">
|
||
<table class="cs-tbl">
|
||
${header}
|
||
<tbody>
|
||
${buildTableRows(data.capd)}
|
||
${buildTotalRow('CAPD TOTAL', capdTot)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Grand Total -->
|
||
<div class="cs-wrap">
|
||
<div class="cs-tbl-scroll">
|
||
<table class="cs-tbl">
|
||
<tbody>
|
||
<tr class="grand-total">
|
||
<td colspan="2" style="font-size:13px;letter-spacing:.3px">TOTAL P&L</td>
|
||
<td class="num">${fmtQty(grand.productionQty)}</td>
|
||
<td class="num">${fmtQty(grand.salesQty)}</td>
|
||
<td class="num">${fmt(grand.rmConsumption)}</td>
|
||
<td class="num">${fmt(grand.employeeCost)}</td>
|
||
<td class="num">${fmt(grand.qaCost)}</td>
|
||
<td class="num">${fmt(grand.qcCost)}</td>
|
||
<td class="num">${fmt(grand.boilerCost)}</td>
|
||
<td class="num">${fmt(grand.powerCost)}</td>
|
||
<td class="num">${fmt(grand.repairCost)}</td>
|
||
<td class="num">${fmt(grand.rdCost)}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
// ── Excel Export ─────────────────────────────────────────────────────────────
|
||
function exportXLSX() {
|
||
if (!pageData) return;
|
||
const from = document.getElementById('fromDate').value;
|
||
const to = document.getElementById('toDate').value;
|
||
|
||
const cols = ['Item Code','Item Description','Production (Units)','Sales Qty','RM Consumption','Employee Cost','QA Cost','QC Cost','Boiler Cost','Power Cost','R&M Cost','R&D Cost'];
|
||
const toRow = (r, isTotal=false) => [
|
||
isTotal ? r.label : r.itemCode,
|
||
isTotal ? '' : (r.itemDescription||''),
|
||
r.productionQty, r.salesQty, r.rmConsumption, r.employeeCost,
|
||
r.qaCost||0, r.qcCost||0, r.boilerCost||0, r.powerCost||0, r.repairCost||0, r.rdCost||0,
|
||
];
|
||
|
||
const bbTot = sectionTotal(pageData.bloodbag);
|
||
const capdTot = sectionTotal(pageData.capd);
|
||
const grand = {
|
||
label:'TOTAL P&L',
|
||
productionQty: bbTot.productionQty + capdTot.productionQty,
|
||
salesQty: bbTot.salesQty + capdTot.salesQty,
|
||
rmConsumption: bbTot.rmConsumption + capdTot.rmConsumption,
|
||
employeeCost: bbTot.employeeCost + capdTot.employeeCost,
|
||
qaCost: bbTot.qaCost + capdTot.qaCost,
|
||
qcCost: bbTot.qcCost + capdTot.qcCost,
|
||
boilerCost: bbTot.boilerCost + capdTot.boilerCost,
|
||
powerCost: bbTot.powerCost + capdTot.powerCost,
|
||
repairCost: bbTot.repairCost + capdTot.repairCost,
|
||
rdCost: bbTot.rdCost + capdTot.rdCost,
|
||
};
|
||
|
||
const empty12 = new Array(12).fill('');
|
||
const rows = [
|
||
cols,
|
||
['BLOOD BAG',...empty12.slice(1)],
|
||
...pageData.bloodbag.map(r => toRow(r)),
|
||
toRow({...bbTot, label:'BLOOD BAG TOTAL'}, true),
|
||
[],
|
||
['CAPD',...empty12.slice(1)],
|
||
...pageData.capd.map(r => toRow(r)),
|
||
toRow({...capdTot, label:'CAPD TOTAL'}, true),
|
||
[],
|
||
toRow(grand, true),
|
||
];
|
||
|
||
const ws = XLSX.utils.aoa_to_sheet(rows);
|
||
ws['!cols'] = [14,36,18,14,18,16,14,14,14,14,14,14].map(w => ({wch:w}));
|
||
|
||
const wb = XLSX.utils.book_new();
|
||
XLSX.utils.book_append_sheet(wb, ws, `Cost Sheet`);
|
||
XLSX.writeFile(wb, `Cost_Sheet_${from}_${to}.xlsx`);
|
||
toast('Excel exported successfully');
|
||
}
|
||
|
||
function printCostSheet() {
|
||
const el = document.getElementById('csContent');
|
||
if (!el || !el.innerHTML.trim()) { toast('Load data first','err'); return; }
|
||
const from = document.getElementById('fromDate').value;
|
||
const to = document.getElementById('toDate').value;
|
||
const w = window.open('','_blank','width=1200,height=800');
|
||
w.document.write(`<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Cost Sheet</title>
|
||
<style>
|
||
@page{size:A4 landscape;margin:6mm}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{font-family:Arial,Helvetica,sans-serif;font-size:7.5px;color:#0f172a;padding:2px}
|
||
h2{font-size:9px;font-weight:700;margin-bottom:4px;color:#1e3264;padding-bottom:2px;border-bottom:1px solid #1e3264}
|
||
.cs-wrap{border:1px solid #e2e8f0;margin-bottom:6px}
|
||
.cs-section-hd{display:flex;align-items:center;gap:5px;padding:3px 6px;background:#f1f5f9;border-bottom:1px solid #e2e8f0;-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
||
.cs-section-tag{padding:1px 5px;border-radius:8px;font-size:7px;font-weight:700;text-transform:uppercase}
|
||
.tag-bb{background:rgba(37,99,235,.1);color:#2563eb;-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
||
.tag-capd{background:rgba(124,58,237,.1);color:#7c3aed;-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
||
.cs-section-label{font-size:8px;font-weight:700;color:#334155}
|
||
.cs-tbl-scroll{overflow:visible!important;max-height:none!important}
|
||
table{width:100%;border-collapse:collapse}
|
||
th,td{font-size:7.5px!important;padding:2px 4px!important;line-height:1.3!important;white-space:nowrap}
|
||
th{text-align:left;background:#f1f5f9!important;color:#475569;font-weight:700;text-transform:uppercase;letter-spacing:.3px;border-bottom:1px solid #e2e8f0;-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
||
th.num,td.num{text-align:right}
|
||
td{border-bottom:1px solid #f8f9fb;color:#1e293b}
|
||
tr.total-row td{background:#f1f5f9!important;font-weight:700;border-top:1.5px solid #e2e8f0;-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
||
tr.grand-total td{background:#1e3264!important;color:#fff!important;font-weight:700;-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
||
</style></head><body>
|
||
<h2>Cost Sheet — ${from} to ${to}</h2>
|
||
${el.innerHTML}
|
||
<script>window.onload=()=>{window.print();}<\/script>
|
||
</body></html>`);
|
||
w.document.close();
|
||
}
|
||
</script>
|
||
<script src="/sidebar.js?v=31"></script>
|
||
</body>
|
||
</html>
|