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

530 lines
35 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/auth-guard.js?v=4"></script>
<script src="/company-list.js"></script>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
<title>Man Power — SAP B1</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Bebas+Neue&display=swap" rel="stylesheet"/>
<style>
:root{--ink:#080e18;--ink2:#0d1626;--ink3:#111f35;--purple:#9f5afd;--blue:#1a6fff;--amber:#f5a623;--green:#0dbb8a;--red:#e53e3e;--teal:#06b6d4;--border:#1c2e48;--border2:#243650;--text:#c8d6e8;--text2:#7a9ab8;--text3:#3d5470;--surface:#0f1d30;--surface2:#142338;--surface3:#0b1520;--radius:10px;--font:'Space Grotesk',sans-serif;--mono:'JetBrains Mono',monospace;--display:'Bebas Neue',sans-serif;--shadow:0 8px 32px rgba(0,0,0,.5)}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:var(--font);background:var(--ink);color:var(--text);min-height:100vh;-webkit-font-smoothing:antialiased}
nav{position:sticky;top:0;z-index:200;background:rgba(8,14,24,.92);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);padding:0 12px;height:50px;display:flex;align-items:center;justify-content:space-between;gap:6px}
.nav-logo{background:var(--blue);color:#fff;font-family:var(--display);font-size:15px;letter-spacing:2px;padding:3px 8px;border-radius:5px}
.nav-links{display:flex;gap:2px;overflow-x:auto;flex:1;justify-content:center}
.nav-link{padding:4px 8px;border-radius:5px;font-size:10px;font-weight:600;color:var(--text2);text-decoration:none;white-space:nowrap;font-family:var(--font)}.nav-link:hover{color:var(--text);background:rgba(255,255,255,.05)}.nav-link.active{color:var(--blue);background:rgba(26,111,255,.1)}
.btn-sm{padding:4px 8px;border-radius:5px;font-size:10px;font-weight:700;border:1px solid var(--border2);color:var(--text3);background:none;cursor:pointer;font-family:var(--font)}.btn-sm:hover{border-color:var(--red);color:var(--red)}
main{padding:12px;max-width:1400px;margin:0 auto}
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:12px;overflow:hidden}
.card-hd{background:var(--surface3);padding:10px 14px;font-size:12px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--border)}
.card-hd .right{margin-left:auto;font-size:11px;color:var(--text3);font-weight:600}
.card-bd{padding:12px 14px}
.fg{margin-bottom:10px}
.fl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:var(--text3);margin-bottom:4px;display:block}
.fi{width:100%;padding:8px 10px;border:1.5px solid var(--border2);border-radius:7px;font-size:13px;font-family:var(--font);background:var(--surface2);color:var(--text);outline:none;height:36px}.fi:focus{border-color:var(--blue);box-shadow:0 0 0 2px rgba(26,111,255,.15)}.fi::placeholder{color:var(--text3)}
select.fi{cursor:pointer;appearance:none;background:var(--surface2) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%233d5470'/%3E%3C/svg%3E") no-repeat right 10px center;padding-right:26px}
.fi-mono{font-family:var(--mono);font-size:12px}
.btn{padding:10px 18px;border-radius:8px;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;justify-content:center}.btn:disabled{opacity:.4;cursor:not-allowed}
.b-primary{background:var(--blue);color:#fff}.b-primary:hover:not(:disabled){background:#155ad6}
.b-cancel{background:var(--surface2);color:var(--text2);border:1px solid var(--border2)}.b-cancel:hover{border-color:var(--text2)}
.b-danger{background:rgba(229,62,62,.1);color:var(--red);border:1px solid rgba(229,62,62,.3)}.b-danger:hover{background:rgba(229,62,62,.2)}
.empty-msg{text-align:center;padding:24px;color:var(--text3);font-size:12px}
.sp{width:14px;height:14px;border:2px solid rgba(255,255,255,.25);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite;display:inline-block}
@keyframes spin{to{transform:rotate(360deg)}}
.toast-c{position:fixed;top:56px;right:10px;z-index:9999;display:flex;flex-direction:column;gap:6px;pointer-events:none}
.toast{padding:9px 13px;border-radius:8px;font-size:12px;font-weight:600;box-shadow:var(--shadow);max-width:300px}
.t-ok{background:var(--green);color:#fff}.t-err{background:var(--red);color:#fff}
/* Form header */
.mp-head{align-items:center}
.mp-head-title{display:flex;align-items:center;gap:11px}
.mp-head-ico{width:34px;height:34px;border-radius:9px;background:rgba(26,111,255,.12);border:1px solid rgba(26,111,255,.25);display:flex;align-items:center;justify-content:center;font-size:17px;flex-shrink:0}
.mp-head-t{font-size:13px;font-weight:700;color:var(--text)}
.mp-head-s{font-size:10.5px;color:var(--text3);margin-top:1px;font-weight:400}
/* Meta panel (date/remark) */
.mp-meta{display:flex;gap:14px;flex-wrap:wrap;align-items:flex-end;padding:14px;background:var(--surface3);border:1px solid var(--border);border-radius:10px;margin-bottom:14px}
.mp-meta .fg{margin:0}
.mp-meta-summary{margin-left:auto;align-self:center;font-size:11px;font-weight:600;color:var(--text2);background:var(--surface2);padding:6px 13px;border-radius:20px;border:1px solid var(--border2);white-space:nowrap}
/* Segmented tabs */
.mp-tabs{display:flex;gap:5px;overflow-x:auto;padding:5px;background:var(--surface3);border:1px solid var(--border);border-radius:11px;margin-bottom:12px}
.mp-tabs::-webkit-scrollbar{height:0}
.mp-tab{padding:8px 13px;font-size:11px;font-weight:700;color:var(--text3);cursor:pointer;white-space:nowrap;border:none;background:none;border-radius:8px;font-family:var(--font);display:flex;align-items:center;gap:7px;transition:all .15s}
.mp-tab:hover{color:var(--text);background:var(--surface2)}
.mp-tab.on{color:#fff;background:var(--blue);box-shadow:0 2px 8px rgba(26,111,255,.35)}
.mp-tab-badge{font-size:9px;font-weight:800;border-radius:9px;padding:1px 6px;min-width:17px;text-align:center;background:var(--blue);color:#fff;line-height:1.5}
.mp-tab.on .mp-tab-badge{background:rgba(255,255,255,.28);color:#fff}
/* Add-row */
.mp-addrow{margin-top:8px;padding:9px 14px;font-size:12px;font-weight:700;color:var(--blue);background:none;border:1.5px dashed var(--border2);border-radius:9px;cursor:pointer;font-family:var(--font);width:100%;transition:all .15s}
.mp-addrow:hover{border-color:var(--blue);background:rgba(26,111,255,.06)}
/* Action bar */
.mp-actions{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap;padding-top:14px;border-top:1px solid var(--border)}
.mp-actions .grow{margin-left:auto}
/* Grid */
.mp-grid-wrap{overflow-x:auto;border:1px solid var(--border);border-radius:8px}
table.mp{border-collapse:separate;border-spacing:0;font-size:11.5px;min-width:1200px;width:100%}
table.mp th{position:sticky;top:0;z-index:1;text-align:left;padding:8px 8px;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:var(--text3);background:var(--surface3);border-bottom:2px solid var(--border2);white-space:nowrap}
table.mp td{padding:4px 5px;border-bottom:1px solid var(--border);vertical-align:middle}
table.mp tbody tr:nth-child(even) td{background:rgba(255,255,255,.02)}
table.mp tbody tr:hover td{background:rgba(26,111,255,.05)}
table.mp th.mp-idx,table.mp td.mp-idx{width:36px;min-width:36px;text-align:center;color:var(--text3);font-size:10px;font-weight:700;font-family:var(--mono)}
table.mp input{width:100%;padding:5px 7px;border:1px solid var(--border2);border-radius:5px;font-size:11px;font-family:var(--mono);background:var(--surface2);color:var(--text);outline:none;height:28px}
table.mp input:focus{border-color:var(--blue)}
table.mp input[readonly]{background:var(--ink2);cursor:not-allowed;color:var(--text2)}
table.mp select{width:100%;padding:5px 22px 5px 7px;border:1px solid var(--border2);border-radius:5px;font-size:11px;font-family:var(--font);background:var(--surface2) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='5'%3E%3Cpath d='M0 0l4.5 5 4.5-5z' fill='%233d5470'/%3E%3C/svg%3E") no-repeat right 7px center;color:var(--text);outline:none;height:28px;cursor:pointer;appearance:none}
table.mp select:focus{border-color:var(--blue)}
table.mp select:disabled{background-color:var(--ink2);cursor:not-allowed;color:var(--text2)}
table.mp select option{background:var(--surface2)}
table.mp select.mp-missing{border-color:var(--red);box-shadow:0 0 0 2px rgba(229,62,62,.18)}
.mp-num{text-align:right}
.mp-rm{width:26px;height:26px;border-radius:5px;border:1px solid var(--border2);background:var(--surface2);color:var(--red);cursor:pointer;font-size:12px}
.mp-rm:hover{background:var(--red);color:#fff;border-color:var(--red)}
/* List table */
.lst{width:100%;border-collapse:separate;border-spacing:0;font-size:12px}
.lst th{text-align:left;padding:8px 10px;font-size:9.5px;font-weight:700;text-transform:uppercase;color:var(--text3);border-bottom:2px solid var(--border2)}
.lst td{padding:9px 10px;border-bottom:1px solid var(--border)}
.lst tr{cursor:pointer}
.lst tr:hover td{background:rgba(26,111,255,.05)}
.pill{font-size:9px;font-weight:700;padding:1px 6px;border-radius:3px}
.pill-ok{background:rgba(13,187,138,.12);color:var(--green)}
.pill-cx{background:rgba(229,62,62,.12);color:var(--red)}
.pill-lg{font-size:10px;padding:3px 10px;border-radius:20px}
/* Professional document list */
.mp-list{display:flex;flex-direction:column;gap:8px}
.mp-doc{display:flex;align-items:center;gap:14px;padding:12px 14px;background:var(--surface2);border:1px solid var(--border);border-radius:12px;cursor:pointer;transition:all .15s}
.mp-doc:hover{border-color:var(--blue);transform:translateY(-1px);box-shadow:0 8px 22px rgba(0,0,0,.28)}
.mp-doc.cx{opacity:.72}
.mp-doc-date{display:flex;flex-direction:column;align-items:center;justify-content:center;width:60px;height:60px;border-radius:12px;background:rgba(26,111,255,.10);border:1px solid rgba(26,111,255,.22);flex-shrink:0}
.mp-doc.cx .mp-doc-date{background:rgba(229,62,62,.08);border-color:rgba(229,62,62,.22)}
.mp-doc-day{font-size:24px;font-weight:700;color:var(--blue);line-height:1;font-family:var(--display);letter-spacing:1.5px}
.mp-doc.cx .mp-doc-day{color:var(--red)}
.mp-doc-mon{font-size:8.5px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:var(--text2);margin-top:3px}
.mp-doc-main{flex:1;min-width:0}
.mp-doc-title{font-size:14px;font-weight:700;color:var(--text)}
.mp-doc-sub{font-size:11px;color:var(--text3);margin-top:4px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.mp-doc-sub b{color:var(--text2);font-family:var(--mono)}
.mp-chips{display:flex;gap:5px;flex-wrap:wrap;margin-top:8px}
.mp-chip{font-size:9px;font-weight:700;color:var(--text2);background:var(--surface3);border:1px solid var(--border2);border-radius:6px;padding:2px 8px;display:inline-flex;align-items:center;gap:5px;text-transform:uppercase;letter-spacing:.3px}
.mp-chip b{color:var(--blue);font-family:var(--mono);font-size:10px}
.mp-doc-right{display:flex;align-items:center;gap:12px;flex-shrink:0}
.mp-total{font-size:11px;font-weight:600;color:var(--text3);background:var(--surface3);border:1px solid var(--border2);border-radius:20px;padding:5px 12px;white-space:nowrap}
.mp-total b{color:var(--text);font-family:var(--mono)}
.mp-doc-chev{color:var(--text3);font-size:20px;line-height:1;transition:transform .15s,color .15s}
.mp-doc:hover .mp-doc-chev{color:var(--blue);transform:translateX(3px)}
.mp-pager{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px}
.mp-count{font-size:11px;font-weight:700;color:var(--blue);background:rgba(26,111,255,.1);padding:3px 11px;border-radius:20px}
/* Compact summary table (row = one Section entry; click to edit in a popup) */
table.mp-sum{width:100%;border-collapse:separate;border-spacing:0;font-size:12.5px}
table.mp-sum th{text-align:left;padding:9px 12px;font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:var(--text3);border-bottom:2px solid var(--border2);white-space:nowrap}
table.mp-sum td{padding:9px 12px;border-bottom:1px solid var(--border);white-space:nowrap}
table.mp-sum tbody tr{cursor:pointer}
table.mp-sum tbody tr:hover td{background:rgba(26,111,255,.06)}
table.mp-sum .num{text-align:right;font-family:var(--mono)}
/* Row entry popup (matches OEE's per-shift popup pattern) */
.mp-mv{position:fixed;inset:0;z-index:9998;background:rgba(4,8,14,.72);backdrop-filter:blur(4px);display:flex;align-items:flex-start;justify-content:center;padding:22px 14px;overflow:auto}
.mp-mw{width:100%;max-width:820px;background:var(--surface);border:1px solid var(--border2);border-radius:14px;box-shadow:var(--shadow);overflow:hidden}
.mp-mh{display:flex;align-items:center;gap:12px;padding:14px 18px;background:var(--surface3);border-bottom:1px solid var(--border)}
.mp-mh-t{font-size:14px;font-weight:700;color:var(--text)}
.mp-mh-s{font-size:11px;color:var(--text3);margin-top:1px}
.mp-mx{width:30px;height:30px;border-radius:8px;border:1px solid var(--border2);background:none;color:var(--text2);cursor:pointer;font-size:14px;margin-left:auto}
.mp-mx:hover{border-color:var(--red);color:var(--red)}
.mp-mb{padding:16px 18px;max-height:calc(100vh - 210px);overflow:auto}
.mp-msec{border:1px solid var(--border);border-radius:11px;overflow:hidden}
.mp-msec-hd{padding:7px 13px;font-size:11px;font-weight:800;letter-spacing:.4px;color:#0a0f18;background:#8aa0c0}
.mp-mgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px;padding:14px}
.mp-mfld{display:flex;flex-direction:column;gap:4px;min-width:0}
.mp-mfld.wide{grid-column:1/-1}
.mp-mfld label{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:var(--text3)}
.mp-mfld input,.mp-mfld select{width:100%;height:36px;padding:6px 9px;border:1.5px solid var(--border2);border-radius:7px;font-size:13px;font-family:var(--font);background:var(--surface2);color:var(--text);outline:none}
.mp-mfld input:focus,.mp-mfld select:focus{border-color:var(--blue)}
.mp-mfld select{cursor:pointer;appearance:none;background:var(--surface2) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%233d5470'/%3E%3C/svg%3E") no-repeat right 10px center;padding-right:26px}
.mp-mfld select.mp-missing{border-color:var(--red);box-shadow:0 0 0 2px rgba(229,62,62,.18)}
.mp-mf{display:flex;gap:8px;padding:14px 18px;border-top:1px solid var(--border);background:var(--surface3)}
.mp-mf .grow{margin-left:auto}
</style>
<link rel="stylesheet" href="/theme.css?v=3"/>
</head>
<body>
<div class="toast-c" id="toast-c"></div>
<nav>
<span class="nav-logo">SAP</span>
<div class="nav-links">
<a href="/" class="nav-link">Home</a><a href="/production" class="nav-link">Production</a>
<a href="/man-power" class="nav-link active">Man Power</a>
</div>
<button class="btn-sm" id="btn-logout">Logout</button>
</nav>
<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='/';
document.getElementById('btn-logout').onclick=()=>{sessionStorage.clear();document.cookie='portal_token=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';document.cookie='portal_user=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';;localStorage.removeItem('token');localStorage.removeItem('portal_user');window.location.href='/';};
function hasStepPerm(fullKey,perm){
if(_user&&_user.role==='admin')return true;
const steps=Array.isArray(_user&&_user.approvalSteps)?_user.approvalSteps:[];
for(const s of steps){
if(typeof s==='string'){ if(s===fullKey) return true; }
else if(s&&s.step===fullKey){ return Array.isArray(s.perms)&&s.perms.includes(perm); }
}
return false;
}
const CAN_VIEW=hasStepPerm('man_power:entry','view');
const CAN_ADD =hasStepPerm('man_power:entry','add');
const CAN_EDIT=hasStepPerm('man_power:entry','edit');
const TABS=[
{key:'bb',label:'BB PRODUCTION'},{key:'packing',label:'PACKING'},{key:'autoclave',label:'AUTOCLAVE'},
{key:'capd',label:'CAPD PRODUCTION'},{key:'moulding',label:'MOULDING'},{key:'sheet',label:'SHEET PLANT'},
{key:'lamination',label:'LAMINATION'},{key:'stent',label:'STENT'},{key:'equipment',label:'EQUIPMENT'},{key:'needle',label:'NEEDLE'},
];
const COLS=[
{k:'section',l:'Section *',type:'text',w:'200px'},
{k:'target',l:'Production Target',type:'num'},
{k:'appManpower',l:'Approved Manpower (Casual + Cont)',type:'num'},
{k:'prntWorker',l:'Permanent Worker',type:'num'},
{k:'totalSaction',l:'Total Sactioned Manpower',type:'num'},
{k:'approvedEt',l:'Approved ET/Day',type:'num'},
{k:'present',l:'Present ManPower StartLink Data',type:'num'},
{k:'overtime',l:'OverTime StarLink Data',type:'num'},
{k:'actual',l:'Actual Production',type:'num'},
{k:'rejection',l:'Rejection',type:'num'},
{k:'remarks',l:'Remarks',type:'text',w:'170px'},
];
function blankRow(){const r={};COLS.forEach(c=>r[c.k]=c.type==='num'?0:'');return r;}
let _co=window.__DEFAULT_COMPANY__;
let _companies=[{value:_co,label:_co}];
function coOptions(sel){return _companies.map(c=>`<option value="${esc(c.value)}" ${sel===c.value?'selected':''}>${esc(c.label)}</option>`).join('');}
let _mode='list'; // 'list' | 'form'
let _docs=[];let _listLoading=false;let _filterDate='';let _page=0,_pageSize=20,_rawCount=0;
let _editEntry=null; // docEntry when editing, null for new
let _date='';let _remark='';let _tabsData={};let _active='bb';
let _canceled=false;let _saving=false;let _formLoading=false;
let _sections={}; // per-tab Section dropdown values (from UFD1)
let _visibleKeys=TABS.map(t=>t.key); // which tabs are shown (admin-configurable)
function visibleTabs(){return TABS.filter(t=>_visibleKeys.includes(t.key));}
function esc(v){return(v||'').toString().replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
function q(s){return document.querySelector(s);}
function mk(c){const d=document.createElement('div');d.className=c;return d;}
function today(){return new Date().toISOString().slice(0,10);}
const _MON=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const _DOW=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
function fmtDate(iso){if(!iso)return '—';const d=new Date(iso);if(isNaN(d))return '—';return `${String(d.getUTCDate()).padStart(2,'0')} ${_MON[d.getUTCMonth()]} ${d.getUTCFullYear()}`;}
function dateParts(iso){const d=new Date(iso);if(isNaN(d))return null;return {day:String(d.getUTCDate()).padStart(2,'0'),mon:_MON[d.getUTCMonth()],year:d.getUTCFullYear(),dow:_DOW[d.getUTCDay()]};}
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.textContent=(type==='ok'?'OK ':'ERR ')+msg;tc.appendChild(el);setTimeout(()=>el.remove(),4500);}
// ── LIST ──────────────────────────────────────────────
async function loadList(resetPage){
if(resetPage)_page=0;
_listLoading=true;render();
const dq=_filterDate?`&date=${_filterDate}`:'';
try{const r=await api('GET',`/manpower?top=${_pageSize}&skip=${_page*_pageSize}&company=${encodeURIComponent(_co)}${dq}`);_docs=r.data||[];_rawCount=_docs.length;}
catch(e){toast(e.message,'err');_docs=[];_rawCount=0;}
_listLoading=false;render();
}
function newEntry(){
_editEntry=null;_date=today();_remark='';_canceled=false;_active=(visibleTabs()[0]||TABS[0]).key;
_tabsData={};TABS.forEach(t=>_tabsData[t.key]=[]);
_mode='form';render();
}
async function openEntry(docEntry){
_mode='form';_formLoading=true;_editEntry=docEntry;render();
try{
const r=await api('GET',`/manpower/${docEntry}?company=${encodeURIComponent(_co)}`);
const d=r.data||{};
_date=(d.date||'').slice(0,10);_remark=d.remark||'';_canceled=!!d.canceled;_active=(visibleTabs()[0]||TABS[0]).key;
_tabsData={};TABS.forEach(t=>{_tabsData[t.key]=(d.tabs&&d.tabs[t.key])||[];});
}catch(e){toast(e.message,'err');_mode='list';}
_formLoading=false;render();
}
async function save(){
if(!_date){toast('Date is required','err');return;}
const editing=_editEntry!=null;
if(editing&&!CAN_EDIT){toast('You are not permitted to edit','err');return;}
if(!editing&&!CAN_ADD){toast('You are not permitted to add','err');return;}
// Every row (added via the popup) already requires a Section — but a row
// loaded from an older, pre-popup document could still lack one, so this
// stays as a final safety check before saving.
let firstBad=null,badCount=0;
for(const t of TABS){
const rows=_tabsData[t.key]||[];
rows.forEach((r,i)=>{ if(!(r.section||'').trim()){ badCount++; if(!firstBad)firstBad={key:t.key,label:t.label,row:i}; } });
}
if(firstBad){
if(_visibleKeys.includes(firstBad.key))_active=firstBad.key;
render();
toast(`Section is required in all rows — ${badCount} row${badCount===1?'':'s'} missing (see ${firstBad.label}, row ${firstBad.row+1}).`,'err');
return;
}
// Nothing to save unless at least one row exists.
const anyValid=TABS.some(t=>(_tabsData[t.key]||[]).length>0);
if(!anyValid){ toast('Add at least one row before saving.','err'); return; }
_saving=true;render();
const body={company:_co,date:_date,remark:_remark,tabs:_tabsData};
try{
if(editing){ await api('PATCH',`/manpower/${_editEntry}`,body); toast('Man Power data updated'); }
else{ const r=await api('POST','/manpower',body); toast(`Saved — Doc #${r.data?.docNum||r.data?.docEntry||''}`); }
_mode='list';_saving=false;loadList(true);
}catch(e){toast(e.message||'Failed','err');_saving=false;render();}
}
async function cancelDoc(){
if(_editEntry==null||!CAN_EDIT)return;
if(!confirm('Cancel this Man Power document in SAP? This cannot be undone.'))return;
_saving=true;render();
try{ await api('POST',`/manpower/${_editEntry}/cancel`,{company:_co}); toast('Document cancelled'); _mode='list';_saving=false;loadList(true); }
catch(e){ toast(e.message||'Failed','err');_saving=false;render(); }
}
// ── RENDER ────────────────────────────────────────────
function render(){try{_doRender();}catch(e){console.error(e);document.getElementById('app').innerHTML=`<div style="padding:20px;color:var(--red)">Error: ${e.message}</div>`;}}
function _doRender(){
const app=document.getElementById('app');app.innerHTML='';
if(!CAN_VIEW){app.innerHTML='<div class="card"><div class="card-bd" style="text-align:center;padding:30px;color:var(--text3)">You are not assigned the "Man Power" permission. Ask an admin to grant it (User Management → Approval Steps → Man Power).</div></div>';return;}
if(_mode==='form')renderForm(app);else renderList(app);
}
function renderList(app){
const c=mk('card');
const docsHtml=_docs.map(d=>{
const p=dateParts(d.date)||{day:'—',mon:'',year:'',dow:''};
const tc=d.tabCounts||{};
// Only show the tabs this user is allowed to see (visibleTabs = global
// enabled ∩ user's assigned) — and total only those rows.
const vis=visibleTabs();
const chips=vis.filter(t=>tc[t.key]).map(t=>`<span class="mp-chip">${t.label} <b>${tc[t.key]}</b></span>`).join('');
const visTotal=vis.reduce((s,t)=>s+(Number(tc[t.key])||0),0);
return `<div class="mp-doc${d.canceled?' cx':''}" data-open="${d.docEntry}">
<div class="mp-doc-date"><div class="mp-doc-day">${p.day}</div><div class="mp-doc-mon">${p.mon} ${p.year}</div></div>
<div class="mp-doc-main">
<div class="mp-doc-title">${p.dow?p.dow+', ':''}${fmtDate(d.date)}</div>
<div class="mp-doc-sub">
<span>Doc <b>#${esc(String(d.docNum))}</b></span>
<span>·</span><span>Created ${fmtDate(d.createDate)}</span>
<span>${d.canceled?'<span class="pill pill-cx pill-lg">CANCELLED</span>':'<span class="pill pill-ok pill-lg">ACTIVE</span>'}</span>
</div>
${chips?`<div class="mp-chips">${chips}</div>`:''}
</div>
<div class="mp-doc-right">${visTotal?`<span class="mp-total"><b>${visTotal}</b> row${visTotal===1?'':'s'}</span>`:''}<span class="mp-doc-chev">›</span></div>
</div>`;
}).join('');
c.innerHTML=`<div class="card-hd"><span>Man Power Data</span><span class="right">${_listLoading?'':`${_docs.length} on this page`}</span></div>
<div class="card-bd">
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end;margin-bottom:14px">
<div class="fg" style="margin:0;min-width:150px"><label class="fl">Company</label><select class="fi" id="sel-co">${coOptions(_co)}</select></div>
<div class="fg" style="margin:0"><label class="fl">Filter by Date</label><input class="fi" id="f-date" type="date" value="${_filterDate}"/></div>
<button class="btn b-cancel" id="f-clear" style="height:36px">Clear</button>
${CAN_ADD?`<button class="btn b-primary" id="btn-new" style="height:36px;margin-left:auto">+ New Entry</button>`:''}
</div>
${_listLoading?`<div class="empty-msg"><span class="sp" style="border-color:rgba(26,111,255,.25);border-top-color:var(--blue)"></span> Loading…</div>`:
!_docs.length?`<div class="empty-msg">No Man Power records${_filterDate?' for '+fmtDate(_filterDate):''}.</div>`:
`<div class="mp-list">${docsHtml}</div>
<div class="mp-pager">
<button class="btn b-cancel" id="l-prev" ${_page===0?'disabled':''} style="padding:6px 14px;font-size:11px">‹ Prev</button>
<span class="mp-count">Page ${_page+1}</span>
<button class="btn b-cancel" id="l-next" ${_rawCount>=_pageSize?'':'disabled'} style="padding:6px 14px;font-size:11px">Next ›</button>
</div>`}
</div>`;
app.appendChild(c);
setTimeout(()=>{
q('#sel-co')&&(q('#sel-co').onchange=function(){_co=this.value;loadList(true);});
q('#f-date')&&(q('#f-date').onchange=function(){_filterDate=this.value;loadList(true);});
q('#f-clear')&&(q('#f-clear').onclick=()=>{_filterDate='';loadList(true);});
q('#btn-new')&&(q('#btn-new').onclick=newEntry);
q('#l-prev')&&(q('#l-prev').onclick=()=>{if(_page>0){_page--;loadList();}});
q('#l-next')&&(q('#l-next').onclick=()=>{if(_rawCount>=_pageSize){_page++;loadList();}});
document.querySelectorAll('[data-open]').forEach(tr=>tr.onclick=()=>openEntry(parseInt(tr.dataset.open)));
},0);
}
const SUM_COLS=['section','target','present','actual','rejection','remarks'];
function renderForm(app){
const editing=_editEntry!=null;
const readOnly=editing?!CAN_EDIT:!CAN_ADD;
const c=mk('card');
const rows=_tabsData[_active]||[];
const sumRows=rows.map((row,i)=>`<tr data-row="${i}">
${SUM_COLS.map(k=>{
const col=COLS.find(cc=>cc.k===k);
const v=row[k];
const disp=k==='section'?(v||'<span style="color:var(--muted)">— not set —</span>'):esc(String(v==null||v===''?(col.type==='num'?0:''):v));
return `<td>${disp}</td>`;
}).join('')}
<td>${readOnly?'':`<button type="button" class="mp-rm" data-rm="${i}" title="Remove row">✕</button>`}</td>
</tr>`).join('');
const totalFilled=visibleTabs().reduce((s,t)=>s+((_tabsData[t.key]||[]).length),0);
c.innerHTML=`<div class="card-hd mp-head">
<div class="mp-head-title">
<span class="mp-head-ico">👷</span>
<div>
<div class="mp-head-t">${editing?'Man Power Data':'New Man Power Entry'}${_canceled?' <span class="pill pill-cx" style="margin-left:4px">CANCELLED</span>':''}</div>
<div class="mp-head-s">${editing?`Document #${_editEntry}`:'Record daily manpower for each production area'}</div>
</div>
</div>
<button class="btn b-cancel" id="btn-back" style="padding:6px 14px;font-size:11px">← Back to list</button>
</div>
<div class="card-bd">
${_formLoading?`<div class="empty-msg"><span class="sp" style="border-color:rgba(26,111,255,.25);border-top-color:var(--blue)"></span> Loading…</div>`:`
<div class="mp-meta">
<div class="fg"><label class="fl">Date <span style="color:var(--red)">*</span></label><input class="fi" id="inp-date" type="date" value="${_date}" ${readOnly?'readonly':''} style="min-width:160px"/></div>
<div class="fg" style="flex:1;min-width:220px"><label class="fl">Remark</label><input class="fi" id="inp-remark" value="${esc(_remark)}" placeholder="Optional note for this day" ${readOnly?'readonly':''}/></div>
<div class="mp-meta-summary">${totalFilled} row${totalFilled===1?'':'s'} total</div>
</div>
<div class="mp-tabs">${visibleTabs().map(t=>{
const cnt=(_tabsData[t.key]||[]).length;
return `<button class="mp-tab${_active===t.key?' on':''}" data-tab="${t.key}">${t.label}${cnt?`<span class="mp-tab-badge">${cnt}</span>`:''}</button>`;
}).join('')}</div>
<div class="mp-grid-wrap"><table class="mp-sum">
<thead><tr>${SUM_COLS.map(k=>`<th>${esc(COLS.find(cc=>cc.k===k).l)}</th>`).join('')}<th></th></tr></thead>
<tbody>${sumRows||`<tr><td colspan="${SUM_COLS.length+1}" style="text-align:center;color:var(--muted);padding:16px">No rows yet${readOnly?'':' — click "+ Add Row" below'}</td></tr>`}</tbody>
</table></div>
${readOnly?'':`<button type="button" class="mp-addrow" id="btn-addrow">+ Add Row</button>`}
<div class="mp-actions">
${readOnly?'':`<button class="btn b-primary" id="btn-save" ${_saving?'disabled':''}><span id="sp-save" style="display:${_saving?'inline-block':'none'}" class="sp"></span> ${editing?'Save Changes':'Save Entry'}</button>`}
${editing&&CAN_EDIT&&!_canceled?`<button class="btn b-danger" id="btn-cancel-doc" ${_saving?'disabled':''}>Cancel Document</button>`:''}
<button class="btn b-cancel${readOnly?'':' grow'}" id="btn-discard">${readOnly?'Close':'Discard'}</button>
</div>
`}
</div>`;
app.appendChild(c);
setTimeout(()=>{
q('#btn-back')&&(q('#btn-back').onclick=()=>{_mode='list';render();});
q('#btn-discard')&&(q('#btn-discard').onclick=()=>{_mode='list';render();});
if(_formLoading)return;
q('#inp-date')&&!readOnly&&(q('#inp-date').onchange=function(){_date=this.value;});
q('#inp-remark')&&!readOnly&&(q('#inp-remark').oninput=function(){_remark=this.value;});
document.querySelectorAll('.mp-tab').forEach(b=>b.onclick=function(){_active=this.dataset.tab;render();});
document.querySelectorAll('table.mp-sum tbody tr[data-row]').forEach(tr=>{
tr.onclick=function(e){
if(e.target.closest('[data-rm]'))return;
openRowModal(parseInt(this.dataset.row),readOnly);
};
});
document.querySelectorAll('[data-rm]').forEach(el=>el.onclick=function(e){
e.stopPropagation();
_tabsData[_active].splice(parseInt(this.dataset.rm),1);
render();
});
q('#btn-addrow')&&(q('#btn-addrow').onclick=()=>openRowModal(null,false));
q('#btn-save')&&(q('#btn-save').onclick=save);
q('#btn-cancel-doc')&&(q('#btn-cancel-doc').onclick=cancelDoc);
},0);
}
// ── Single-row entry popup ──────────────────────────
function openRowModal(index,readOnly){
const editing=index!=null&&index>=0;
const work=editing?JSON.parse(JSON.stringify(_tabsData[_active][index])):blankRow();
const secOpts=_sections[_active]||[];
const fldHtml=(col)=>{
const wide=col.k==='remarks';
let inp;
if(col.k==='section'){
const cur=work.section||'';
const inList=secOpts.includes(cur);
inp=`<select data-f="section">
<option value="">— select —</option>
${secOpts.map(s=>`<option value="${esc(s)}" ${cur===s?'selected':''}>${esc(s)}</option>`).join('')}
${cur&&!inList?`<option value="${esc(cur)}" selected>${esc(cur)}</option>`:''}
</select>`;
}else{
inp=`<input data-f="${col.k}" ${col.type==='num'?'type="number" step="any"':'type="text"'} value="${esc(String(work[col.k]!=null?work[col.k]:(col.type==='num'?0:'')))}" ${readOnly?'disabled':''}/>`;
}
if(col.k==='section')inp=inp.replace('<select ',`<select ${readOnly?'disabled':''} `);
return `<div class="mp-mfld${wide?' wide':''}"><label>${esc(col.l)}</label>${inp}</div>`;
};
const ov=document.createElement('div');ov.className='mp-mv';
ov.innerHTML=`<div class="mp-mw">
<div class="mp-mh">
<div>
<div class="mp-mh-t">${editing?'Edit Row':'Add Row'}</div>
<div class="mp-mh-s">${esc((visibleTabs().find(t=>t.key===_active)||{}).label||'')} · ${esc(fmtDate(_date))}</div>
</div>
<button class="mp-mx" data-x title="Close">✕</button>
</div>
<div class="mp-mb">
<div class="mp-msec">
<div class="mp-msec-hd">Man Power Details</div>
<div class="mp-mgrid">${COLS.map(fldHtml).join('')}</div>
</div>
</div>
<div class="mp-mf">
${readOnly?'':`<button class="btn b-primary" data-save>${editing?'Update Row':'Add Row'}</button>`}
<button class="btn b-cancel${readOnly?'':' grow'}" data-cancel>${readOnly?'Close':'Cancel'}</button>
</div>
</div>`;
document.body.appendChild(ov);
ov.querySelectorAll('[data-f]').forEach(el=>{
el.oninput=el.onchange=function(){
const k=this.dataset.f;const col=COLS.find(c=>c.k===k);
work[k]=col.type==='num'?(parseFloat(this.value)||0):this.value;
if(k==='section')this.classList.remove('mp-missing');
};
});
const close=()=>ov.remove();
ov.querySelector('[data-x]').onclick=close;
ov.querySelector('[data-cancel]').onclick=close;
const saveBtn=ov.querySelector('[data-save]');
if(saveBtn)saveBtn.onclick=()=>{
if(!(work.section||'').toString().trim()){
const se=ov.querySelector('[data-f="section"]');if(se){se.classList.add('mp-missing');se.focus();}
toast('Section is required for this row','err');return;
}
if(editing)_tabsData[_active][index]=work;else _tabsData[_active].push(work);
close();render();
};
}
async function loadSections(){
try{const r=await api('GET',`/manpower/sections?company=${encodeURIComponent(_co)}`);_sections=r.data||{};}
catch(_e){_sections={};}
}
async function loadConfig(){
let globalKeys=TABS.map(t=>t.key);
try{const cfg=await api('GET','/config');
if(cfg&&Array.isArray(cfg.manpowerTabs)&&cfg.manpowerTabs.length)globalKeys=cfg.manpowerTabs;
}catch(_e){}
// Per-user tab permission (Admin → user → Man Power). Empty = all. The user
// sees the INTERSECTION of globally-enabled tabs and their allowed tabs.
let userKeys=null;
try{const pr=await api('GET','/auth/profile');const ut=pr?.user?.manpowerTabs;if(Array.isArray(ut)&&ut.length)userKeys=ut.map(String);}catch(_e){}
_visibleKeys=userKeys?globalKeys.filter(k=>userKeys.includes(k)):globalKeys;
if(!_visibleKeys.includes(_active))_active=(visibleTabs()[0]||TABS[0]).key;
}
async function init(){
try{
try{const list=await window.fetchCompanyList();if(list.length)_companies=list;}catch(_e){}
if(CAN_VIEW){ await Promise.all([loadSections(),loadConfig()]); loadList(); } else render();
}catch(e){console.error('Init error:',e);render();}
}
init();
</script>
<script src="/sidebar.js?v=25"></script>
</body>
</html>