530 lines
36 KiB
HTML
530 lines
36 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<script src="/auth-guard.js?v=4"></script>
|
||
<script src="/company-list.js"></script>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
|
||
<title>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 _docsTotal=null;
|
||
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
||
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;_docsTotal=typeof r.total==='number'?r.total:null;}
|
||
catch(e){toast(e.message,'err');_docs=[];_rawCount=0;_docsTotal=null;}
|
||
_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">${_docsTotal!=null?`Page ${_page+1} of ${Math.max(1,Math.ceil(_docsTotal/_pageSize))} · ${_docsTotal} record${_docsTotal===1?'':'s'}`:`Page ${_page+1}`}</span>
|
||
<button class="btn b-cancel" id="l-next" ${(_docsTotal!=null?(_page+1)*_pageSize<_docsTotal:_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=31"></script>
|
||
</body>
|
||
</html>
|