<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HR Performance Dashboard v4</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pptxgenjs@3.12.0/dist/pptxgen.bundle.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500;600;700&display=swap');
:root{
--bg:#0a0a0f;--bg2:#111118;--card:#14141e;--card2:#1a1a28;
--border:#252535;--text:#d4d4e8;--dim:#6b6b8a;
--cyan:#00d4ff;--pink:#ff2d78;--purple:#7c3aff;
--green:#00ff9d;--orange:#ff6b35;--yellow:#ffd700;
--amber:#ffab40;--violet:#e040fb;
}
*{box-sizing:border-box;margin:0;padding:0;}
body{background:var(--bg);color:var(--text);font-family:'DM Sans',sans-serif;min-height:100vh;overflow-x:hidden;}
body::before{content:'';position:fixed;top:-50%;left:-50%;width:200%;height:200%;
background:radial-gradient(ellipse at 15% 15%,rgba(0,212,255,.05) 0%,transparent 50%),
radial-gradient(ellipse at 85% 85%,rgba(124,58,255,.05) 0%,transparent 50%);
pointer-events:none;z-index:0;}
.wrap{position:relative;z-index:1;}
::-webkit-scrollbar{width:5px;height:5px;}
::-webkit-scrollbar-track{background:var(--bg2);}
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px;}
/* ── HEADER ── */
header{padding:16px 28px 0;display:flex;align-items:center;justify-content:space-between;
border-bottom:1px solid var(--border);background:rgba(10,10,15,.97);
backdrop-filter:blur(12px);position:sticky;top:0;z-index:200;}
.logo{display:flex;align-items:center;gap:11px;}
.logo-icon{width:36px;height:36px;background:linear-gradient(135deg,var(--cyan),var(--purple));
border-radius:8px;display:flex;align-items:center;justify-content:center;
font-family:'Space Mono',monospace;font-size:13px;font-weight:700;color:#fff;}
.logo-text{font-family:'Space Mono',monospace;font-size:12px;letter-spacing:2px;text-transform:uppercase;}
.logo-text span{color:var(--cyan);}
.header-right{display:flex;align-items:center;gap:10px;padding-bottom:4px;}
.hdr-mode{font-family:'Space Mono',monospace;font-size:9px;color:var(--dim);}
.hdr-mode strong{color:var(--cyan);display:block;font-size:10px;}
/* EXPORT BUTTONS */
.export-btns{display:flex;gap:8px;}
.exp-btn{display:flex;align-items:center;gap:6px;padding:7px 14px;border-radius:7px;
font-family:'Space Mono',monospace;font-size:9px;letter-spacing:1px;text-transform:uppercase;
cursor:pointer;border:1px solid;transition:.2s;font-weight:700;white-space:nowrap;}
.exp-btn:hover{transform:translateY(-1px);}
.exp-btn:active{transform:translateY(0);}
.exp-btn.pdf{background:rgba(255,45,120,.1);border-color:rgba(255,45,120,.4);color:var(--pink);}
.exp-btn.pdf:hover{background:rgba(255,45,120,.18);border-color:var(--pink);box-shadow:0 0 12px rgba(255,45,120,.25);}
.exp-btn.pptx{background:rgba(255,107,53,.1);border-color:rgba(255,107,53,.4);color:var(--orange);}
.exp-btn.pptx:hover{background:rgba(255,107,53,.18);border-color:var(--orange);box-shadow:0 0 12px rgba(255,107,53,.25);}
.exp-btn.all{background:rgba(0,212,255,.08);border-color:rgba(0,212,255,.35);color:var(--cyan);}
.exp-btn.all:hover{background:rgba(0,212,255,.14);border-color:var(--cyan);box-shadow:0 0 12px rgba(0,212,255,.2);}
.exp-icon{font-size:13px;}
/* EXPORT LOADING OVERLAY */
.export-overlay{display:none;position:fixed;inset:0;z-index:9999;
background:rgba(10,10,15,.85);backdrop-filter:blur(6px);
align-items:center;justify-content:center;flex-direction:column;gap:16px;}
.export-overlay.show{display:flex;}
.export-spinner{width:44px;height:44px;border:3px solid var(--border);
border-top-color:var(--cyan);border-radius:50%;animation:spin .8s linear infinite;}
@keyframes spin{to{transform:rotate(360deg);}}
.export-msg{font-family:'Space Mono',monospace;font-size:11px;letter-spacing:2px;
text-transform:uppercase;color:var(--cyan);}
.export-sub{font-size:11px;color:var(--dim);}
/* ── UPLOAD ── */
.upload-wrap{margin:16px 28px;background:var(--card);border:1px solid var(--border);border-radius:11px;overflow:hidden;}
.upload-head{display:flex;align-items:center;justify-content:space-between;padding:13px 18px;
cursor:pointer;user-select:none;border-bottom:1px solid transparent;transition:.2s;}
.upload-head.open{border-bottom-color:var(--border);}
.upload-head-left{display:flex;align-items:center;gap:8px;font-family:'Space Mono',monospace;
font-size:10px;letter-spacing:2px;text-transform:uppercase;color:var(--cyan);}
.upload-head-left::before{content:'';width:5px;height:5px;background:var(--cyan);border-radius:50%;box-shadow:0 0 7px var(--cyan);}
.upload-arrow{font-size:10px;color:var(--dim);transition:.2s;}
.upload-arrow.open{transform:rotate(180deg);}
.upload-body{display:none;padding:16px;}
.upload-body.open{display:block;}
.upload-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:9px;}
.u-item{background:var(--bg2);border:1px solid var(--border);border-radius:7px;padding:10px 12px;transition:.2s;}
.u-item:hover{border-color:var(--cyan);}
.u-item.master{border-color:rgba(0,212,255,.35);background:rgba(0,212,255,.03);}
.u-label{font-size:9px;font-family:'Space Mono',monospace;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;display:block;}
.u-item.master .u-label{color:var(--cyan);}
.u-item input[type=file]{width:100%;font-size:10px;color:var(--text);background:transparent;border:none;outline:none;cursor:pointer;}
.u-item input[type=file]::file-selector-button{background:var(--card2);border:1px solid var(--border);color:var(--text);
padding:3px 8px;border-radius:3px;font-size:8px;cursor:pointer;margin-right:6px;
font-family:'Space Mono',monospace;transition:.2s;}
.u-item input[type=file]::file-selector-button:hover{background:var(--cyan);color:#000;}
/* ── STATUS ── */
.status-bar{display:flex;gap:8px;padding:0 28px;margin-bottom:-4px;flex-wrap:wrap;}
.s-pill{background:var(--card);border:1px solid var(--border);border-radius:20px;
padding:4px 12px;font-size:9px;font-family:'Space Mono',monospace;color:var(--dim);
display:flex;align-items:center;gap:5px;}
.s-dot{width:5px;height:5px;border-radius:50%;background:var(--green);box-shadow:0 0 5px var(--green);}
.s-dot.off{background:var(--dim);box-shadow:none;}
/* ── TABS ── */
.tab-bar{display:flex;padding:0 28px;border-bottom:1px solid var(--border);margin-top:16px;overflow-x:auto;}
.tab-btn{padding:12px 20px;font-family:'Space Mono',monospace;font-size:9px;letter-spacing:1.5px;
text-transform:uppercase;color:var(--dim);background:transparent;border:none;cursor:pointer;
position:relative;transition:.2s;white-space:nowrap;flex-shrink:0;}
.tab-btn::after{content:'';position:absolute;bottom:-1px;left:0;right:0;height:2px;
background:var(--cyan);transform:scaleX(0);transition:.25s ease;box-shadow:0 0 8px var(--cyan);}
.tab-btn:hover{color:var(--text);}
.tab-btn.active{color:var(--cyan);}
.tab-btn.active::after{transform:scaleX(1);}
.tab-btn.t-ins.active{color:var(--green);}
.tab-btn.t-ins.active::after{background:var(--green);box-shadow:0 0 8px var(--green);}
.tab-btn.t-instr.active{color:var(--amber);}
.tab-btn.t-instr.active::after{background:var(--amber);box-shadow:0 0 8px var(--amber);}
.tab-pane{display:none;padding:24px 28px;}
.tab-pane.active{display:block;}
/* ── SECTION TITLE ── */
.sec-t{font-family:'Space Mono',monospace;font-size:9px;letter-spacing:3px;text-transform:uppercase;
color:var(--dim);margin-bottom:14px;display:flex;align-items:center;gap:10px;}
.sec-t::after{content:'';flex:1;height:1px;background:var(--border);}
/* ── KPI STRIP ── */
.kpi-strip{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;margin-bottom:20px;}
.kpi-card{background:var(--card);border:1px solid var(--border);border-radius:9px;padding:13px 15px;
position:relative;overflow:hidden;transition:.2s;}
.kpi-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;}
.kpi-card.c-cyan::before{background:linear-gradient(90deg,var(--cyan),transparent);}
.kpi-card.c-green::before{background:linear-gradient(90deg,var(--green),transparent);}
.kpi-card.c-purple::before{background:linear-gradient(90deg,var(--purple),transparent);}
.kpi-card.c-amber::before{background:linear-gradient(90deg,var(--amber),transparent);}
.kpi-card.c-pink::before{background:linear-gradient(90deg,var(--pink),transparent);}
.kpi-card:hover{border-color:rgba(0,212,255,.2);}
.kpi-lbl{font-family:'Space Mono',monospace;font-size:7px;letter-spacing:1.5px;text-transform:uppercase;color:var(--dim);}
.kpi-val{font-family:'Space Mono',monospace;font-size:20px;font-weight:700;color:var(--text);line-height:1.1;margin:3px 0 2px;}
.kpi-sub{font-size:9px;color:var(--dim);}
.kpi-ico{position:absolute;right:12px;top:12px;font-size:16px;opacity:.2;}
/* ── EMPLOYEE CARDS ── */
.emp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:16px;}
.emp-card{background:var(--card);border:1px solid var(--border);border-radius:11px;padding:16px;
transition:.2s;position:relative;overflow:hidden;}
.emp-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;
background:linear-gradient(90deg,var(--cyan),var(--purple));opacity:0;transition:.2s;}
.emp-card:hover{border-color:rgba(0,212,255,.3);transform:translateY(-2px);}
.emp-card:hover::before{opacity:1;}
.emp-head{display:flex;align-items:center;gap:10px;margin-bottom:10px;}
.emp-av{width:40px;height:40px;border-radius:9px;display:flex;align-items:center;justify-content:center;
font-family:'Space Mono',monospace;font-size:13px;font-weight:700;flex-shrink:0;}
.emp-nm{font-size:13px;font-weight:600;}
.emp-id{font-family:'Space Mono',monospace;font-size:9px;color:var(--dim);margin-top:1px;}
.emp-badges{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap;}
.e-badge{background:var(--card2);border:1px solid var(--border);border-radius:5px;padding:4px 9px;
font-family:'Space Mono',monospace;font-size:8px;display:flex;flex-direction:column;gap:1px;}
.e-badge .bl{color:var(--dim);}
.e-badge .bv{font-size:12px;font-weight:700;}
.e-badge.tot .bv{color:var(--cyan);}
.e-badge.avg .bv{color:var(--green);}
.e-badge.best .bv{color:var(--yellow);}
.e-badge.weak .bv{color:var(--pink);}
.emp-ch{position:relative;height:190px;}
/* ── SERVICE TAB ── */
.svc-tot-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(165px,1fr));gap:9px;margin-bottom:20px;}
.stc{background:var(--card);border:1px solid var(--border);border-radius:9px;padding:12px 14px;}
.stc-lbl{font-family:'Space Mono',monospace;font-size:7px;letter-spacing:1px;text-transform:uppercase;color:var(--dim);margin-bottom:3px;}
.stc-val{font-family:'Space Mono',monospace;font-size:17px;font-weight:700;color:var(--cyan);}
.stc-avg{font-size:9px;color:var(--dim);margin-top:1px;}
.svc-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:16px;}
.svc-card{background:var(--card);border:1px solid var(--border);border-radius:11px;padding:16px;transition:.2s;}
.svc-card:hover{border-color:rgba(0,212,255,.2);}
.svc-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:11px;padding-bottom:9px;border-bottom:1px solid var(--border);}
.svc-name{font-family:'Space Mono',monospace;font-size:9px;letter-spacing:2px;text-transform:uppercase;color:var(--cyan);}
.svc-stats{display:flex;gap:10px;}
.svc-stat{font-family:'Space Mono',monospace;font-size:8px;color:var(--dim);display:flex;flex-direction:column;align-items:flex-end;}
.svc-stat strong{font-size:11px;color:var(--text);}
.svc-ch{position:relative;height:170px;}
/* ── DISTRIBUTION ── */
.dist-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:16px;}
.dist-card{background:var(--card);border:1px solid var(--border);border-radius:11px;padding:16px;transition:.2s;}
.dist-card:hover{border-color:rgba(124,58,255,.3);}
.dist-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:11px;padding-bottom:9px;border-bottom:1px solid var(--border);}
.dist-name{font-family:'Space Mono',monospace;font-size:9px;letter-spacing:1.5px;text-transform:uppercase;color:var(--purple);}
.dist-tot{font-family:'Space Mono',monospace;font-size:8px;color:var(--dim);background:var(--card2);padding:2px 8px;border-radius:3px;}
.dist-ch{position:relative;height:190px;}
/* ── INSIGHTS ── */
.ins-kpi{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px;margin-bottom:22px;}
.ik{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px 16px;
display:flex;gap:12px;align-items:center;transition:.2s;position:relative;overflow:hidden;}
.ik::before{content:'';position:absolute;top:0;left:0;bottom:0;width:3px;}
.ik.green::before{background:var(--green);}
.ik.cyan::before{background:var(--cyan);}
.ik.pink::before{background:var(--pink);}
.ik.amber::before{background:var(--amber);}
.ik.purple::before{background:var(--purple);}
.ik:hover{border-color:rgba(0,212,255,.2);}
.ik-ico{font-size:20px;flex-shrink:0;}
.ik-lbl{font-family:'Space Mono',monospace;font-size:7px;letter-spacing:1.5px;text-transform:uppercase;color:var(--dim);margin-bottom:2px;}
.ik-val{font-family:'Space Mono',monospace;font-size:17px;font-weight:700;}
.ik-sub{font-size:9px;color:var(--dim);margin-top:1px;}
.ins-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(400px,1fr));gap:16px;margin-bottom:20px;}
.ins-card{background:var(--card);border:1px solid var(--border);border-radius:11px;padding:16px;transition:.2s;}
.ins-card:hover{border-color:rgba(0,255,157,.15);}
.ins-title{font-family:'Space Mono',monospace;font-size:8px;letter-spacing:2px;text-transform:uppercase;
color:var(--green);margin-bottom:12px;padding-bottom:9px;border-bottom:1px solid var(--border);
display:flex;align-items:center;gap:7px;}
.ins-title span{font-size:13px;}
.ins-ch{position:relative;height:210px;}
.lb-wrap{background:var(--card);border:1px solid var(--border);border-radius:11px;padding:16px;margin-bottom:16px;}
.lb-title{font-family:'Space Mono',monospace;font-size:8px;letter-spacing:2px;text-transform:uppercase;color:var(--amber);margin-bottom:12px;padding-bottom:9px;border-bottom:1px solid var(--border);}
.lb-row{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid rgba(37,37,53,.5);}
.lb-row:last-child{border-bottom:none;}
.lb-rank{font-family:'Space Mono',monospace;font-size:10px;font-weight:700;width:22px;text-align:center;flex-shrink:0;}
.lb-rank.r1{color:var(--yellow);}
.lb-rank.r2{color:#c0c0c0;}
.lb-rank.r3{color:#cd7f32;}
.lb-rank.rn{color:var(--dim);}
.lb-av{width:28px;height:28px;border-radius:6px;display:flex;align-items:center;justify-content:center;
font-family:'Space Mono',monospace;font-size:10px;font-weight:700;flex-shrink:0;}
.lb-info{flex:1;min-width:0;}
.lb-name{font-size:12px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.lb-sub{font-size:9px;color:var(--dim);}
.lb-bar-wrap{flex:1;background:var(--bg2);border-radius:3px;height:5px;max-width:100px;}
.lb-bar{height:100%;border-radius:3px;}
.lb-score{font-family:'Space Mono',monospace;font-size:11px;font-weight:700;min-width:34px;text-align:right;}
.radar-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:16px;}
.radar-card{background:var(--card);border:1px solid var(--border);border-radius:11px;padding:14px;transition:.2s;}
.radar-card:hover{border-color:rgba(124,58,255,.25);}
.radar-head{display:flex;align-items:center;gap:9px;margin-bottom:10px;}
.radar-av{width:30px;height:30px;border-radius:7px;display:flex;align-items:center;justify-content:center;
font-family:'Space Mono',monospace;font-size:10px;font-weight:700;flex-shrink:0;}
.radar-nm{font-size:11px;font-weight:600;}
.radar-sc{font-family:'Space Mono',monospace;font-size:9px;color:var(--dim);}
.radar-ch{position:relative;height:190px;}
/* ── INSTRUCTIONS ── */
.intr-intro{background:linear-gradient(135deg,rgba(255,171,64,.06),rgba(255,171,64,.02));
border:1px solid rgba(255,171,64,.2);border-radius:11px;padding:16px 20px;margin-bottom:18px;
display:flex;gap:12px;align-items:flex-start;}
.ii-title{font-family:'Space Mono',monospace;font-size:9px;letter-spacing:2px;text-transform:uppercase;color:var(--amber);margin-bottom:5px;}
.ii-text{font-size:11px;color:var(--dim);line-height:1.7;}
.ii-text strong{color:var(--text);}
.sub-tabs{display:flex;border-bottom:1px solid var(--border);margin-bottom:18px;overflow-x:auto;}
.st-btn{padding:8px 16px;font-family:'Space Mono',monospace;font-size:8px;letter-spacing:1.5px;
text-transform:uppercase;color:var(--dim);background:transparent;border:none;cursor:pointer;
position:relative;transition:.2s;white-space:nowrap;}
.st-btn::after{content:'';position:absolute;bottom:-1px;left:0;right:0;height:1px;background:var(--amber);transform:scaleX(0);transition:.2s;}
.st-btn.active{color:var(--amber);}
.st-btn.active::after{transform:scaleX(1);}
.sub-panel{display:none;}
.sub-panel.active{display:block;}
.master-layout{display:grid;grid-template-columns:1.1fr .9fr;gap:16px;}
@media(max-width:760px){.master-layout{grid-template-columns:1fr;}}
.i-card{background:var(--card);border:1px solid var(--border);border-radius:11px;overflow:hidden;}
.i-card-head{display:flex;align-items:center;justify-content:space-between;padding:11px 15px;background:var(--card2);border-bottom:1px solid var(--border);}
.i-card-title{font-family:'Space Mono',monospace;font-size:9px;letter-spacing:1px;color:var(--text);}
.badge{font-family:'Space Mono',monospace;font-size:7px;padding:2px 7px;border-radius:3px;text-transform:uppercase;letter-spacing:.5px;}
.bm{background:rgba(0,212,255,.12);color:var(--cyan);border:1px solid rgba(0,212,255,.25);}
.bs{background:rgba(255,171,64,.12);color:var(--amber);border:1px solid rgba(255,171,64,.25);}
.bt{background:rgba(107,107,138,.12);color:var(--dim);border:1px solid rgba(107,107,138,.2);}
.i-card-body{padding:14px;}
.col-tbl{width:100%;border-collapse:collapse;}
.col-tbl th{font-family:'Space Mono',monospace;font-size:7px;letter-spacing:1.5px;text-transform:uppercase;color:var(--dim);padding:6px 10px;text-align:left;border-bottom:1px solid var(--border);background:rgba(255,255,255,.02);}
.col-tbl td{padding:7px 10px;font-size:10px;color:var(--text);border-bottom:1px solid rgba(37,37,53,.5);vertical-align:top;}
.col-tbl tr:last-child td{border-bottom:none;}
.col-tbl tr:hover td{background:rgba(255,255,255,.015);}
.cn{font-family:'Space Mono',monospace;font-size:9px;color:var(--cyan);}
.cn.a{color:var(--amber);}
.ct{font-family:'Space Mono',monospace;font-size:8px;color:var(--purple);}
.cd{color:var(--dim);font-size:10px;line-height:1.5;}
.req-y{background:rgba(255,45,120,.1);color:#ff7aaa;font-family:'Space Mono',monospace;font-size:7px;padding:2px 5px;border-radius:2px;}
.req-n{background:rgba(107,107,138,.12);color:var(--dim);font-family:'Space Mono',monospace;font-size:7px;padding:2px 5px;border-radius:2px;}
.csv-pre{background:#0d0d16;border:1px solid var(--border);border-radius:7px;padding:11px 13px;margin-top:10px;overflow-x:auto;}
.csv-pre-lbl{font-family:'Space Mono',monospace;font-size:7px;letter-spacing:1.5px;text-transform:uppercase;color:var(--dim);margin-bottom:5px;}
.csv-pre pre{font-family:'Space Mono',monospace;font-size:9.5px;line-height:1.8;white-space:pre;}
.ch{color:var(--amber);}
.cr{color:#6b9bff;}
.svc-instr-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:12px;}
.si-card{background:var(--card);border:1px solid var(--border);border-radius:10px;overflow:hidden;transition:.2s;}
.si-card:hover{border-color:rgba(255,171,64,.2);}
.si-head{display:flex;align-items:center;gap:7px;padding:10px 13px;background:var(--card2);border-bottom:1px solid var(--border);}
.si-num{font-family:'Space Mono',monospace;font-size:8px;color:var(--dim);background:rgba(255,255,255,.05);padding:1px 5px;border-radius:2px;}
.si-name{font-family:'Space Mono',monospace;font-size:9px;color:var(--text);flex:1;}
.si-file{font-family:'Space Mono',monospace;font-size:8px;color:var(--amber);background:rgba(255,171,64,.08);padding:2px 7px;border-radius:3px;border:1px solid rgba(255,171,64,.2);white-space:nowrap;}
.si-body{padding:12px 13px;}
.si-desc{font-size:10px;color:var(--dim);line-height:1.6;margin-bottom:9px;}
.si-cols{display:flex;flex-direction:column;gap:5px;margin-bottom:9px;}
.si-col{display:flex;align-items:flex-start;gap:7px;padding:5px 8px;background:rgba(255,255,255,.02);border-radius:5px;}
.si-cn{font-family:'Space Mono',monospace;font-size:8px;color:var(--amber);min-width:110px;flex-shrink:0;}
.si-ci{flex:1;}
.si-ct{font-family:'Space Mono',monospace;font-size:7px;color:var(--purple);margin-bottom:1px;}
.si-cd{font-size:9px;color:var(--dim);line-height:1.4;}
.si-req{font-family:'Space Mono',monospace;font-size:7px;padding:1px 5px;border-radius:2px;flex-shrink:0;align-self:center;}
.si-sample{background:#0d0d16;border:1px solid var(--border);border-radius:5px;padding:8px 10px;overflow-x:auto;}
.si-sample pre{font-family:'Space Mono',monospace;font-size:9px;line-height:1.8;white-space:pre;}
.rules-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;margin-bottom:18px;}
.rule{background:var(--card);border:1px solid var(--border);border-radius:9px;padding:14px;display:flex;gap:11px;transition:.2s;}
.rule:hover{border-color:rgba(0,212,255,.2);}
.rule-ico{width:32px;height:32px;border-radius:7px;display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0;}
.ri-c{background:rgba(0,212,255,.1);}
.ri-a{background:rgba(255,171,64,.1);}
.ri-p{background:rgba(124,58,255,.1);}
.ri-g{background:rgba(0,255,157,.08);}
.rule-title{font-family:'Space Mono',monospace;font-size:8px;letter-spacing:1px;text-transform:uppercase;color:var(--text);margin-bottom:4px;}
.rule-desc{font-size:10px;color:var(--dim);line-height:1.6;}
.tip-box{background:rgba(0,255,157,.04);border:1px solid rgba(0,255,157,.15);border-radius:9px;padding:12px 16px;display:flex;gap:10px;}
.tip-txt{font-size:11px;color:var(--dim);line-height:1.7;}
.tip-txt strong{color:var(--green);}
</style>
</head>
<body>
<div class="wrap">
<!-- EXPORT OVERLAY -->
<div class="export-overlay" id="expOverlay">
<div class="export-spinner"></div>
<div class="export-msg" id="expMsg">Generating...</div>
<div class="export-sub" id="expSub">Please wait</div>
</div>
<!-- HEADER -->
<header>
<div class="logo">
<div class="logo-icon">HR</div>
<div class="logo-text">Performance <span>Analytics</span></div>
</div>
<div class="header-right">
<div class="export-btns">
<button class="exp-btn pdf" onclick="exportPDF()"><span class="exp-icon">📄</span>Export PDF</button>
<button class="exp-btn pptx" onclick="exportPPTX()"><span class="exp-icon">📊</span>Export PPTX</button>
<button class="exp-btn all" onclick="exportBoth()"><span class="exp-icon">⬇</span>Export All</button>
</div>
<div class="hdr-mode"><strong id="hdrMode">MOCK DATA</strong>v4.0</div>
</div>
</header>
<!-- UPLOAD -->
<div class="upload-wrap">
<div class="upload-head" id="upHead">
<div class="upload-head-left">CSV Data Import</div>
<div class="upload-arrow" id="upArrow">▼</div>
</div>
<div class="upload-body" id="upBody">
<div class="upload-grid">
<div class="u-item master"><label class="u-label">★ Master CSV</label><input type="file" accept=".csv" id="up_master"></div>
<div class="u-item"><label class="u-label">Hiring</label><input type="file" accept=".csv" id="up_hiring"></div>
<div class="u-item"><label class="u-label">Status Update</label><input type="file" accept=".csv" id="up_employee_status_update"></div>
<div class="u-item"><label class="u-label">Adv. Children Edu</label><input type="file" accept=".csv" id="up_advance_children_education"></div>
<div class="u-item"><label class="u-label">Education Settlement</label><input type="file" accept=".csv" id="up_education_settlement"></div>
<div class="u-item"><label class="u-label">Wages</label><input type="file" accept=".csv" id="up_wages"></div>
<div class="u-item"><label class="u-label">Work Schedule / OT</label><input type="file" accept=".csv" id="up_work_schedule_ot"></div>
<div class="u-item"><label class="u-label">Internal Transfer</label><input type="file" accept=".csv" id="up_internal_transfer"></div>
<div class="u-item"><label class="u-label">HOP</label><input type="file" accept=".csv" id="up_hop"></div>
<div class="u-item"><label class="u-label">Separation</label><input type="file" accept=".csv" id="up_separation"></div>
<div class="u-item"><label class="u-label">Grievance</label><input type="file" accept=".csv" id="up_grievance"></div>
<div class="u-item"><label class="u-label">Investigation Cases</label><input type="file" accept=".csv" id="up_investigation_cases"></div>
</div>
</div>
</div>
<!-- STATUS -->
<div class="status-bar">
<div class="s-pill"><span class="s-dot"></span>7 Employees</div>
<div class="s-pill"><span class="s-dot"></span>11 Services</div>
<div class="s-pill" id="modePill"><span class="s-dot off"></span>Mock Data</div>
<div class="s-pill"><span class="s-dot" style="background:var(--amber);box-shadow:0 0 5px var(--amber)"></span>Grand Total: <strong id="gtVal" style="color:var(--amber);font-family:'Space Mono',monospace;font-size:9px;margin-left:3px;">—</strong></div>
</div>
<!-- TAB BAR -->
<div class="tab-bar">
<button class="tab-btn active" data-tab="tp-emp">Per Employee</button>
<button class="tab-btn" data-tab="tp-svc">Per Service</button>
<button class="tab-btn" data-tab="tp-dist">Distribution</button>
<button class="tab-btn t-ins" data-tab="tp-insights">Insights</button>
<button class="tab-btn t-instr" data-tab="tp-instr">CSV Instructions</button>
</div>
<!-- TAB 1 -->
<div class="tab-pane active" id="tp-emp">
<div class="sec-t">KPI Overview</div>
<div class="kpi-strip" id="empKpi"></div>
<div class="sec-t">Employee Performance Cards</div>
<div class="emp-grid" id="empGrid"></div>
</div>
<!-- TAB 2 -->
<div class="tab-pane" id="tp-svc">
<div class="sec-t">Service Totals</div>
<div class="svc-tot-grid" id="svcTotals"></div>
<div class="sec-t">Service Comparison Charts</div>
<div class="svc-grid" id="svcGrid"></div>
</div>
<!-- TAB 3 -->
<div class="tab-pane" id="tp-dist">
<div class="sec-t">Distribution by Service</div>
<div class="dist-grid" id="distGrid"></div>
</div>
<!-- TAB 4: INSIGHTS -->
<div class="tab-pane" id="tp-insights">
<div class="sec-t">Performance Intelligence</div>
<div class="ins-kpi" id="insKpi"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;">
<div class="lb-wrap"><div class="lb-title">🏆 Overall Ranking</div><div id="lbList"></div></div>
<div class="ins-card"><div class="ins-title"><span>📡</span>Team Average by Service</div><div class="ins-ch"><canvas id="teamAvgChart"></canvas></div></div>
</div>
<div class="sec-t">Individual Radar Profiles</div>
<div class="radar-grid" id="radarGrid"></div>
<div class="sec-t">Comparative Insights</div>
<div class="ins-grid" id="insGrid"></div>
</div>
<!-- TAB 5: INSTRUCTIONS -->
<div class="tab-pane" id="tp-instr">
<div class="intr-intro">
<div style="font-size:20px;flex-shrink:0;">📋</div>
<div>
<div class="ii-title">CSV Format Guide</div>
<div class="ii-text">Upload a <strong>Master CSV</strong> (all 11 services) or <strong>individual service CSVs</strong>. Service files override master for that service. Headers are <strong>case-insensitive</strong>; spaces normalize to underscores.</div>
</div>
</div>
<div class="sub-tabs">
<button class="st-btn active" data-panel="sp-master">Master CSV</button>
<button class="st-btn" data-panel="sp-services">Service CSVs</button>
<button class="st-btn" data-panel="sp-rules">Merge Rules</button>
</div>
<div class="sub-panel active" id="sp-master">
<div class="master-layout">
<div class="i-card">
<div class="i-card-head"><span class="i-card-title">master.csv — Column Reference</span><span class="badge bm">Master</span></div>
<div class="i-card-body">
<table class="col-tbl">
<thead><tr><th>Column</th><th>Type</th><th>Req</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="cn">employee_id</span></td><td><span class="ct">string</span></td><td><span class="req-y">Yes</span></td><td class="cd">Primary key — unique per employee, used for cross-file merging.</td></tr>
<tr><td><span class="cn">employee_name</span></td><td><span class="ct">string</span></td><td><span class="req-y">Yes</span></td><td class="cd">Full display name shown in all cards and charts.</td></tr>
<tr><td><span class="cn">hiring</span></td><td><span class="ct">number</span></td><td><span class="req-y">Yes</span></td><td class="cd">Score for Hiring service (0–100).</td></tr>
<tr><td><span class="cn">employee_status_update</span></td><td><span class="ct">number</span></td><td><span class="req-y">Yes</span></td><td class="cd">Score for status change transactions.</td></tr>
<tr><td><span class="cn">advance_children_education</span></td><td><span class="ct">number</span></td><td><span class="req-y">Yes</span></td><td class="cd">Advance on children's education benefit score.</td></tr>
<tr><td><span class="cn">education_settlement</span></td><td><span class="ct">number</span></td><td><span class="req-y">Yes</span></td><td class="cd">Education reimbursement settlement score.</td></tr>
<tr><td><span class="cn">wages</span></td><td><span class="ct">number</span></td><td><span class="req-y">Yes</span></td><td class="cd">Wage processing / payroll score.</td></tr>
<tr><td><span class="cn">work_schedule_ot</span></td><td><span class="ct">number</span></td><td><span class="req-y">Yes</span></td><td class="cd">Work schedule and overtime approval score.</td></tr>
<tr><td><span class="cn">internal_transfer</span></td><td><span class="ct">number</span></td><td><span class="req-y">Yes</span></td><td class="cd">Internal transfer requests score.</td></tr>
<tr><td><span class="cn">hop</span></td><td><span class="ct">number</span></td><td><span class="req-y">Yes</span></td><td class="cd">HOP (Head of Personnel) transaction score.</td></tr>
<tr><td><span class="cn">separation</span></td><td><span class="ct">number</span></td><td><span class="req-y">Yes</span></td><td class="cd">Separation / offboarding case score.</td></tr>
<tr><td><span class="cn">grievance</span></td><td><span class="ct">number</span></td><td><span class="req-y">Yes</span></td><td class="cd">Grievance cases handled score.</td></tr>
<tr><td><span class="cn">investigation_cases</span></td><td><span class="ct">number</span></td><td><span class="req-y">Yes</span></td><td class="cd">Investigation cases closed score.</td></tr>
</tbody>
</table>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:12px;">
<div class="i-card">
<div class="i-card-head"><span class="i-card-title">Sample — master.csv</span><span class="badge bt">Example</span></div>
<div class="i-card-body">
<div class="csv-pre"><div class="csv-pre-lbl">CSV Preview</div>
<pre><span class="ch">employee_id,employee_name,hiring,employee_status_update,advance_children_education,education_settlement,wages,work_schedule_ot,internal_transfer,hop,separation,grievance,investigation_cases</span>
<span class="cr">EMP001,Ahmed Al-Rashidi,85,72,90,68,77,55,80,62,71,45,58</span>
<span class="cr">EMP002,Sara Binsaleh,91,88,74,82,95,70,65,79,83,60,72</span>
<span class="cr">EMP003,Omar Khalidi,67,55,80,73,60,85,90,50,66,77,88</span></pre>
</div>
</div>
</div>
<div class="i-card">
<div class="i-card-head"><span class="i-card-title">Formatting Notes</span><span class="badge bt">Tips</span></div>
<div class="i-card-body">
<div class="si-cols" style="gap:5px;">
<div class="si-col"><div class="si-cn" style="color:var(--cyan)">Header row</div><div class="si-ci"><div class="si-cd">Must be line 1. Auto-lowercased; spaces → underscores.</div></div></div>
<div class="si-col"><div class="si-cn" style="color:var(--cyan)">Delimiter</div><div class="si-ci"><div class="si-cd">Comma only. No tabs or semicolons.</div></div></div>
<div class="si-col"><div class="si-cn" style="color:var(--cyan)">Values</div><div class="si-ci"><div class="si-cd">Integers or decimals. Empty → 0.</div></div></div>
<div class="si-col"><div class="si-cn" style="color:var(--cyan)">Encoding</div><div class="si-ci"><div class="si-cd">UTF-8 without BOM recommended.</div></div></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="sub-panel" id="sp-services">
<div class="i-card" style="margin-bottom:16px;">
<div class="i-card-head"><span class="i-card-title">Universal 3-Column Structure — all service CSVs</span><span class="badge bs">All Services</span></div>
<div class="i-card-body">
<table class="col-tbl">
<thead><tr><th>Column</th><th>Type</th><th>Req</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="cn a">employee_id</span></td><td><span class="ct">string</span></td><td><span class="req-y">Yes</span></td><td class="cd">Unique employee ID. Must match existing data for correct merging.</td></tr>
<tr><td><span class="cn a">employee_name</span></td><td><span class="ct">string</span></td><td><span class="req-n">Opt</span></td><td class="cd">Full name. Updates display if provided; preserved if omitted.</td></tr>
<tr><td><span class="cn a">value</span></td><td><span class="ct">number</span></td><td><span class="req-y">Yes</span></td><td class="cd">Score for this specific service. Always named <code style="color:var(--amber)">value</code>.</td></tr>
</tbody>
</table>
</div>
</div>
<div class="svc-instr-grid" id="svcInstrGrid"></div>
</div>
<div class="sub-panel" id="sp-rules">
<div class="rules-grid">
<div class="rule"><div class="rule-ico ri-c">📥</div><div><div class="rule-title">Master CSV Priority</div><div class="rule-desc">Populates all 11 services for all employees at once. Baseline dataset.</div></div></div>
<div class="rule"><div class="rule-ico ri-a">🔄</div><div><div class="rule-title">Service Override</div><div class="rule-desc">Individual CSV overrides only that service. Others remain unchanged.</div></div></div>
<div class="rule"><div class="rule-ico ri-p">🆕</div><div><div class="rule-title">Auto-Create Employee</div><div class="rule-desc">Unknown employee_id in service CSV creates employee with 0s elsewhere.</div></div></div>
<div class="rule"><div class="rule-ico ri-g">🎭</div><div><div class="rule-title">Mock Fallback</div><div class="rule-desc">No CSV? Randomized mock data for 7 employees across 11 services.</div></div></div>
<div class="rule"><div class="rule-ico ri-c">🔁</div><div><div class="rule-title">Auto Re-render</div><div class="rule-desc">All tabs, charts, totals, PDF/PPTX use fresh data on every upload.</div></div></div>
<div class="rule"><div class="rule-ico ri-a">⚠️</div><div><div class="rule-title">Empty → 0</div><div class="rule-desc">Non-numeric or empty cells silently default to 0.</div></div></div>
</div>
<div class="tip-box">
<div style="font-size:16px;flex-shrink:0;">💡</div>
<div class="tip-txt"><strong>Recommended:</strong> Use master.csv for a full data load, then individual service CSVs for incremental updates. The Export PDF and Export PPTX buttons always reflect the current data state.</div>
</div>
</div>
</div>
</div><!-- /wrap -->
<script>
/* ═══════════════════ CONFIG ═══════════════════ */
const SVCS=[
{key:'hiring', label:'Hiring', file:'hiring.csv', desc:'Hiring transaction scores per employee.'},
{key:'employee_status_update',label:'Status Update', file:'employee_status_update.csv',desc:'Employee status change and update scores.'},
{key:'advance_children_education',label:'Adv.Child Edu',file:'advance_children_education.csv',desc:'Advance on children\'s education benefit.'},
{key:'education_settlement',label:'Edu Settlement', file:'education_settlement.csv', desc:'Education reimbursement settlement scores.'},
{key:'wages', label:'Wages', file:'wages.csv', desc:'Wage processing and payroll scores.'},
{key:'work_schedule_ot', label:'Work Sched/OT', file:'work_schedule_ot.csv', desc:'Work schedule and overtime approval scores.'},
{key:'internal_transfer', label:'Int. Transfer', file:'internal_transfer.csv', desc:'Internal transfer request scores.'},
{key:'hop', label:'HOP', file:'hop.csv', desc:'Head of Personnel transaction scores.'},
{key:'separation', label:'Separation', file:'separation.csv', desc:'Separation and offboarding case scores.'},
{key:'grievance', label:'Grievance', file:'grievance.csv', desc:'Grievance cases handled scores.'},
{key:'investigation_cases', label:'Investigation', file:'investigation_cases.csv', desc:'Investigation cases closed scores.'}
];
const ECOLS=['00d4ff','ff2d78','7c3aff','00ff9d','ff6b35','ffd700','e040fb'];
const ECOLS_CSS=ECOLS.map(c=>`#${c}`);
const AVT=[
'linear-gradient(135deg,#00d4ff,#7c3aff)',
'linear-gradient(135deg,#ff2d78,#ff6b35)',
'linear-gradient(135deg,#7c3aff,#e040fb)',
'linear-gradient(135deg,#00ff9d,#00d4ff)',
'linear-gradient(135deg,#ff6b35,#ffd700)',
'linear-gradient(135deg,#ffd700,#ff2d78)',
'linear-gradient(135deg,#e040fb,#7c3aff)'
];
const MOCK=[
{id:'EMP001',name:'Ahmed Al-Rashidi'},{id:'EMP002',name:'Sara Binsaleh'},
{id:'EMP003',name:'Omar Khalidi'},{id:'EMP004',name:'Nora Al-Farsi'},
{id:'EMP005',name:'Yousef Mansour'},{id:'EMP006',name:'Lina Qureshi'},
{id:'EMP007',name:'Tariq Bashir'}
];
function rnd(a,b){return Math.floor(Math.random()*(b-a+1))+a;}
function mockData(){const d={};MOCK.forEach(e=>{d[e.id]={id:e.id,name:e.name};SVCS.forEach(s=>{d[e.id][s.key]=rnd(30,100);});});return d;}
let DATA=mockData();
const CH={emp:{},svc:{},dist:{},ins:{}};
/* helpers */
function EA(){return Object.values(DATA);}
function eTotal(e){return SVCS.reduce((s,sv)=>s+(e[sv.key]||0),0);}
function eAvg(e){return eTotal(e)/SVCS.length;}
function eBest(e){return SVCS.reduce((b,s)=>(e[s.key]||0)>(e[b.key]||0)?s:b,SVCS[0]);}
function eWorst(e){return SVCS.reduce((w,s)=>(e[s.key]||0)<(e[w.key]||0)?s:w,SVCS[0]);}
function sTotal(k){return EA().reduce((s,e)=>s+(e[k]||0),0);}
function sAvg(k){return sTotal(k)/EA().length;}
function GT(){return EA().reduce((s,e)=>s+eTotal(e),0);}
function INI(n){return n.split(' ').map(w=>w[0]).join('').slice(0,2).toUpperCase();}
function idx(e){return EA().indexOf(e);}
Chart.defaults.color='#6b6b8a';
Chart.defaults.borderColor='#252535';
Chart.defaults.font.family='DM Sans';
const CB=()=>({responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},tooltip:{backgroundColor:'#1a1a28',borderColor:'#252535',borderWidth:1,titleColor:'#d4d4e8',bodyColor:'#6b6b8a',padding:9,cornerRadius:5}}});
function destroy(o){Object.values(o).forEach(c=>{try{c&&c.destroy&&c.destroy();}catch(e){}});}
/* ═══ KPI STRIP ═══ */
function renderKpi(){
const arr=EA(),gt=GT(),avg=(gt/(arr.length*SVCS.length)).toFixed(1);
const sorted=[...arr].sort((a,b)=>eTotal(b)-eTotal(a));
const top=sorted[0],bot=sorted[sorted.length-1];
const bSvc=SVCS.reduce((b,s)=>sTotal(s.key)>sTotal(b.key)?s:b,SVCS[0]);
const wSvc=SVCS.reduce((b,s)=>sTotal(s.key)<sTotal(b.key)?s:b,SVCS[0]);
document.getElementById('empKpi').innerHTML=`
<div class="kpi-card c-cyan"><div class="kpi-lbl">Grand Total</div><div class="kpi-val">${gt}</div><div class="kpi-sub">All employees × all services</div><div class="kpi-ico">∑</div></div>
<div class="kpi-card c-green"><div class="kpi-lbl">Team Average</div><div class="kpi-val">${avg}</div><div class="kpi-sub">Per employee per service</div><div class="kpi-ico">~</div></div>
<div class="kpi-card c-amber"><div class="kpi-lbl">Top Performer</div><div class="kpi-val">${top.name.split(' ')[0]}</div><div class="kpi-sub">Total: ${eTotal(top)}</div><div class="kpi-ico">🏆</div></div>
<div class="kpi-card c-pink"><div class="kpi-lbl">Needs Support</div><div class="kpi-val">${bot.name.split(' ')[0]}</div><div class="kpi-sub">Total: ${eTotal(bot)}</div><div class="kpi-ico">📌</div></div>
<div class="kpi-card c-cyan"><div class="kpi-lbl">Strongest Svc</div><div class="kpi-val">${bSvc.label}</div><div class="kpi-sub">Total: ${sTotal(bSvc.key)}</div><div class="kpi-ico">⬆</div></div>
<div class="kpi-card c-purple"><div class="kpi-lbl">Weakest Svc</div><div class="kpi-val">${wSvc.label}</div><div class="kpi-sub">Total: ${sTotal(wSvc.key)}</div><div class="kpi-ico">⬇</div></div>
`;
document.getElementById('gtVal').textContent=gt;
}
/* ═══ TAB 1 ═══ */
function renderEmp(){
renderKpi(); destroy(CH.emp); CH.emp={};
const g=document.getElementById('empGrid'); g.innerHTML='';
EA().forEach((emp,i)=>{
const t=eTotal(emp),a=eAvg(emp).toFixed(1),b=eBest(emp),w=eWorst(emp);
const c=document.createElement('div'); c.className='emp-card';
c.innerHTML=`<div class="emp-head"><div class="emp-av" style="background:${AVT[i%AVT.length]};color:#fff">${INI(emp.name)}</div><div><div class="emp-nm">${emp.name}</div><div class="emp-id">${emp.id}</div></div></div>
<div class="emp-badges">
<div class="e-badge tot"><span class="bl">TOTAL</span><span class="bv">${t}</span></div>
<div class="e-badge avg"><span class="bl">AVG</span><span class="bv">${a}</span></div>
<div class="e-badge best"><span class="bl">BEST</span><span class="bv">${b.label}</span></div>
<div class="e-badge weak"><span class="bl">WEAK</span><span class="bv">${w.label}</span></div>
</div>
<div class="emp-ch"><canvas id="ec-${emp.id}"></canvas></div>`;
g.appendChild(c);
const ctx=c.querySelector(`#ec-${emp.id}`).getContext('2d');
const col=ECOLS_CSS[i%ECOLS_CSS.length];
CH.emp[emp.id]=new Chart(ctx,{type:'bar',data:{labels:SVCS.map(s=>s.label),datasets:[{data:SVCS.map(s=>emp[s.key]||0),backgroundColor:SVCS.map((_,j)=>`${col}${(70+j*8).toString(16).padStart(2,'0')}`),borderColor:col,borderWidth:1,borderRadius:4,borderSkipped:false}]},options:{...CB(),indexAxis:'y',scales:{x:{beginAtZero:true,max:100,grid:{color:'#1e1e30'},ticks:{font:{size:8}}},y:{grid:{display:false},ticks:{font:{size:8}}}}}});
});
}
/* ═══ TAB 2 ═══ */
function renderSvc(){
destroy(CH.svc); CH.svc={};
const ts=document.getElementById('svcTotals'); ts.innerHTML='';
SVCS.forEach(s=>{const t=sTotal(s.key),a=sAvg(s.key).toFixed(1);const d=document.createElement('div');d.className='stc';d.innerHTML=`<div class="stc-lbl">${s.label}</div><div class="stc-val">${t}</div><div class="stc-avg">avg ${a}/emp</div>`;ts.appendChild(d);});
const g=document.getElementById('svcGrid'); g.innerHTML='';
SVCS.forEach(s=>{
const emps=EA(),vals=emps.map(e=>e[s.key]||0),t=sTotal(s.key),a=sAvg(s.key).toFixed(1);
const c=document.createElement('div');c.className='svc-card';
c.innerHTML=`<div class="svc-head"><div class="svc-name">${s.label}</div><div class="svc-stats"><div class="svc-stat"><span>TOTAL</span><strong>${t}</strong></div><div class="svc-stat"><span>AVG</span><strong>${a}</strong></div></div></div><div class="svc-ch"><canvas id="sc-${s.key}"></canvas></div>`;
g.appendChild(c);
const ctx=c.querySelector(`#sc-${s.key}`).getContext('2d');
CH.svc[s.key]=new Chart(ctx,{type:'bar',data:{labels:emps.map(e=>e.name.split(' ')[0]),datasets:[{data:vals,backgroundColor:ECOLS_CSS.map(c=>c+'99'),borderColor:ECOLS_CSS,borderWidth:1.5,borderRadius:5,borderSkipped:false}]},options:{...CB(),scales:{y:{beginAtZero:true,max:100,grid:{color:'#1e1e30'},ticks:{font:{size:8}}},x:{grid:{display:false},ticks:{font:{size:9}}}}}});
});
}
/* ═══ TAB 3 ═══ */
function renderDist(){
destroy(CH.dist); CH.dist={};
const g=document.getElementById('distGrid'); g.innerHTML='';
SVCS.forEach(s=>{
const emps=EA(),vals=emps.map(e=>e[s.key]||0),t=sTotal(s.key);
const c=document.createElement('div');c.className='dist-card';
c.innerHTML=`<div class="dist-head"><div class="dist-name">${s.label}</div><div class="dist-tot">Total: ${t}</div></div><div class="dist-ch"><canvas id="dc-${s.key}"></canvas></div>`;
g.appendChild(c);
const ctx=c.querySelector(`#dc-${s.key}`).getContext('2d');
CH.dist[s.key]=new Chart(ctx,{type:'doughnut',data:{labels:emps.map(e=>e.name.split(' ')[0]),datasets:[{data:vals,backgroundColor:ECOLS_CSS.map(c=>c+'cc'),borderColor:'#14141e',borderWidth:2}]},options:{...CB(),cutout:'60%',plugins:{...CB().plugins,legend:{display:true,position:'bottom',labels:{color:'#9090b0',font:{size:8},boxWidth:8,padding:6}}}}});
});
}
/* ═══ TAB 4: INSIGHTS ═══ */
function renderInsights(){
const arr=EA(),sorted=[...arr].sort((a,b)=>eTotal(b)-eTotal(a));
const gt=GT(),pct=((gt/(arr.length*SVCS.length*100))*100).toFixed(1);
const top=sorted[0],bot=sorted[sorted.length-1],bSvc=SVCS.reduce((b,s)=>sTotal(s.key)>sTotal(b.key)?s:b,SVCS[0]);
document.getElementById('insKpi').innerHTML=`
<div class="ik green"><div class="ik-ico">🎯</div><div><div class="ik-lbl">Efficiency</div><div class="ik-val" style="color:var(--green)">${pct}%</div><div class="ik-sub">of max possible</div></div></div>
<div class="ik cyan"><div class="ik-ico">📊</div><div><div class="ik-lbl">Grand Total</div><div class="ik-val" style="color:var(--cyan)">${gt}</div><div class="ik-sub">${arr.length}emp × ${SVCS.length}svc</div></div></div>
<div class="ik amber"><div class="ik-ico">🏆</div><div><div class="ik-lbl">Top Performer</div><div class="ik-val" style="color:var(--amber)">${top.name.split(' ')[0]}</div><div class="ik-sub">Score ${eTotal(top)}</div></div></div>
<div class="ik pink"><div class="ik-ico">📌</div><div><div class="ik-lbl">Needs Support</div><div class="ik-val" style="color:var(--pink)">${bot.name.split(' ')[0]}</div><div class="ik-sub">Gap: ${eTotal(top)-eTotal(bot)}</div></div></div>
<div class="ik purple"><div class="ik-ico">⬆</div><div><div class="ik-lbl">Best Service</div><div class="ik-val" style="color:var(--purple)">${bSvc.label}</div><div class="ik-sub">Total ${sTotal(bSvc.key)}</div></div></div>
`;
/* leaderboard */
const lb=document.getElementById('lbList'); lb.innerHTML='';
const maxT=eTotal(sorted[0]);
sorted.forEach((emp,i)=>{
const t=eTotal(emp),ei=arr.indexOf(emp),col=ECOLS_CSS[ei%ECOLS_CSS.length];
const rc=i===0?'r1':i===1?'r2':i===2?'r3':'rn';
const ri=i===0?'🥇':i===1?'🥈':i===2?'🥉':`#${i+1}`;
lb.innerHTML+=`<div class="lb-row"><div class="lb-rank ${rc}">${ri}</div><div class="lb-av" style="background:${AVT[ei%AVT.length]};color:#fff">${INI(emp.name)}</div><div class="lb-info"><div class="lb-name">${emp.name}</div><div class="lb-sub">Avg ${eAvg(emp).toFixed(1)} · Best: ${eBest(emp).label}</div></div><div class="lb-bar-wrap"><div class="lb-bar" style="width:${(t/maxT*100).toFixed(0)}%;background:${col}"></div></div><div class="lb-score" style="color:${col}">${t}</div></div>`;
});
/* team avg chart */
if(CH.ins.ta) CH.ins.ta.destroy();
CH.ins.ta=new Chart(document.getElementById('teamAvgChart').getContext('2d'),{type:'bar',data:{labels:SVCS.map(s=>s.label),datasets:[{data:SVCS.map(s=>sAvg(s.key)),backgroundColor:SVCS.map((_,i)=>`${ECOLS_CSS[i%ECOLS_CSS.length]}99`),borderColor:SVCS.map((_,i)=>ECOLS_CSS[i%ECOLS_CSS.length]),borderWidth:1.5,borderRadius:4,borderSkipped:false}]},options:{...CB(),indexAxis:'y',scales:{x:{beginAtZero:true,max:100,grid:{color:'#1e1e30'},ticks:{font:{size:8}}},y:{grid:{display:false},ticks:{font:{size:8}}}}}});
/* radar grid */
destroy(CH.ins.radars||{}); CH.ins.radars={};
const rg=document.getElementById('radarGrid'); rg.innerHTML='';
arr.forEach((emp,i)=>{
const col=ECOLS_CSS[i%ECOLS_CSS.length];
const c=document.createElement('div');c.className='radar-card';
c.innerHTML=`<div class="radar-head"><div class="radar-av" style="background:${AVT[i%AVT.length]};color:#fff">${INI(emp.name)}</div><div><div class="radar-nm">${emp.name}</div><div class="radar-sc">Total: ${eTotal(emp)} · Avg: ${eAvg(emp).toFixed(1)}</div></div></div><div class="radar-ch"><canvas id="rc-${emp.id}"></canvas></div>`;
rg.appendChild(c);
CH.ins.radars[emp.id]=new Chart(c.querySelector(`#rc-${emp.id}`).getContext('2d'),{type:'radar',data:{labels:SVCS.map(s=>s.label),datasets:[{data:SVCS.map(s=>emp[s.key]||0),borderColor:col,backgroundColor:col+'22',pointBackgroundColor:col,pointRadius:3,borderWidth:2}]},options:{...CB(),scales:{r:{beginAtZero:true,max:100,grid:{color:'#252535'},angleLines:{color:'#252535'},pointLabels:{color:'#6b6b8a',font:{size:7}},ticks:{backdropColor:'transparent',color:'#6b6b8a',font:{size:7},stepSize:25}}}}});
});
/* insight charts */
destroy(CH.ins.extra||{}); CH.ins.extra={};
const ig=document.getElementById('insGrid'); ig.innerHTML='';
// totals
const cA=document.createElement('div');cA.className='ins-card';cA.innerHTML=`<div class="ins-title"><span>📈</span>Total Score per Employee</div><div class="ins-ch"><canvas id="ins-tot"></canvas></div>`;ig.appendChild(cA);
const srtd=[...arr].sort((a,b)=>eTotal(b)-eTotal(a));
CH.ins.extra.tot=new Chart(cA.querySelector('#ins-tot').getContext('2d'),{type:'bar',data:{labels:srtd.map(e=>e.name.split(' ')[0]),datasets:[{data:srtd.map(e=>eTotal(e)),backgroundColor:srtd.map(e=>ECOLS_CSS[arr.indexOf(e)%ECOLS_CSS.length]+'bb'),borderColor:srtd.map(e=>ECOLS_CSS[arr.indexOf(e)%ECOLS_CSS.length]),borderWidth:1.5,borderRadius:5,borderSkipped:false}]},options:{...CB(),scales:{y:{beginAtZero:true,grid:{color:'#1e1e30'},ticks:{font:{size:8}}},x:{grid:{display:false},ticks:{font:{size:9}}}}}});
// svc volume
const cB=document.createElement('div');cB.className='ins-card';cB.innerHTML=`<div class="ins-title"><span>🌐</span>Service Volume (Polar)</div><div class="ins-ch"><canvas id="ins-pol"></canvas></div>`;ig.appendChild(cB);
CH.ins.extra.pol=new Chart(cB.querySelector('#ins-pol').getContext('2d'),{type:'polarArea',data:{labels:SVCS.map(s=>s.label),datasets:[{data:SVCS.map(s=>sTotal(s.key)),backgroundColor:ECOLS_CSS.map(c=>c+'55'),borderColor:ECOLS_CSS,borderWidth:1.5}]},options:{...CB(),scales:{r:{beginAtZero:true,grid:{color:'#252535'},ticks:{backdropColor:'transparent',color:'#6b6b8a',font:{size:7}}}},plugins:{...CB().plugins,legend:{display:true,position:'right',labels:{color:'#9090b0',font:{size:8},boxWidth:8,padding:6}}}}});
// gap
const cC=document.createElement('div');cC.className='ins-card';cC.innerHTML=`<div class="ins-title"><span>⚡</span>Performance Gap per Service</div><div class="ins-ch"><canvas id="ins-gap"></canvas></div>`;ig.appendChild(cC);
const gaps=SVCS.map(s=>{const v=arr.map(e=>e[s.key]||0);return Math.max(...v)-Math.min(...v);});
CH.ins.extra.gap=new Chart(cC.querySelector('#ins-gap').getContext('2d'),{type:'bar',data:{labels:SVCS.map(s=>s.label),datasets:[{data:gaps,backgroundColor:gaps.map(g=>g>40?'#ff2d7899':g>20?'#ffab4099':'#00ff9d88'),borderColor:gaps.map(g=>g>40?'#ff2d78':g>20?'#ffab40':'#00ff9d'),borderWidth:1.5,borderRadius:4,borderSkipped:false}]},options:{...CB(),indexAxis:'y',scales:{x:{beginAtZero:true,grid:{color:'#1e1e30'},ticks:{font:{size:8}}},y:{grid:{display:false},ticks:{font:{size:8}}}}}});
// avg line
const cD=document.createElement('div');cD.className='ins-card';cD.innerHTML=`<div class="ins-title"><span>📉</span>Average Score per Employee</div><div class="ins-ch"><canvas id="ins-avg"></canvas></div>`;ig.appendChild(cD);
CH.ins.extra.avg=new Chart(cD.querySelector('#ins-avg').getContext('2d'),{type:'line',data:{labels:arr.map(e=>e.name.split(' ')[0]),datasets:[{data:arr.map(e=>parseFloat(eAvg(e).toFixed(1))),borderColor:'#00d4ff',backgroundColor:'rgba(0,212,255,.08)',pointBackgroundColor:ECOLS_CSS,pointRadius:6,pointHoverRadius:8,borderWidth:2,tension:.35,fill:true}]},options:{...CB(),scales:{y:{beginAtZero:true,max:100,grid:{color:'#1e1e30'},ticks:{font:{size:8}}},x:{grid:{display:false},ticks:{font:{size:9}}}}}});
}
/* ═══ INSTRUCTIONS SERVICE CARDS ═══ */
function renderInstrCards(){
const g=document.getElementById('svcInstrGrid'); g.innerHTML='';
SVCS.forEach((s,i)=>{
const c=document.createElement('div');c.className='si-card';
c.innerHTML=`<div class="si-head"><span class="si-num">#${String(i+1).padStart(2,'0')}</span><span class="si-name">${s.label}</span><span class="si-file">${s.file}</span></div>
<div class="si-body"><div class="si-desc">${s.desc}</div>
<div class="si-cols">
<div class="si-col"><div class="si-cn">employee_id</div><div class="si-ci"><div class="si-ct">string·required</div><div class="si-cd">Unique ID matching master data.</div></div><span class="si-req" style="background:rgba(255,45,120,.12);color:#ff7aaa">REQ</span></div>
<div class="si-col"><div class="si-cn">employee_name</div><div class="si-ci"><div class="si-ct">string·optional</div><div class="si-cd">Updates display name if provided.</div></div><span class="si-req" style="background:rgba(107,107,138,.12);color:var(--dim)">OPT</span></div>
<div class="si-col"><div class="si-cn">value</div><div class="si-ci"><div class="si-ct">number·required</div><div class="si-cd">Score for <strong style="color:var(--text)">${s.label}</strong>.</div></div><span class="si-req" style="background:rgba(255,45,120,.12);color:#ff7aaa">REQ</span></div>
</div>
<div class="si-sample"><pre><span class="ch">employee_id,employee_name,value</span>\n<span class="cr">EMP001,Ahmed Al-Rashidi,85</span>\n<span class="cr">EMP002,Sara Binsaleh,91</span></pre></div></div>`;
g.appendChild(c);
});
}
function renderAll(){renderEmp();renderSvc();renderDist();renderInsights();}
/* ═══════════════════════════
CSV
═══════════════════════════ */
function parseCSV(txt){
const lines=txt.trim().split('\n').map(l=>l.trim()).filter(Boolean);
if(!lines.length)return[];
const hdrs=lines[0].split(',').map(h=>h.trim().toLowerCase().replace(/\s+/g,'_'));
return lines.slice(1).map(line=>{const v=line.split(',').map(c=>c.trim());const o={};hdrs.forEach((h,i)=>{o[h]=v[i]||'';});return o;});
}
function applyMaster(rows){const nd={};rows.forEach(r=>{const id=r.employee_id||r.id,name=r.employee_name||r.name||id;if(!id)return;nd[id]={id,name};SVCS.forEach(s=>{nd[id][s.key]=parseFloat(r[s.key])||0;});});if(Object.keys(nd).length)DATA=nd;}
function applySvc(key,rows){rows.forEach(r=>{const id=r.employee_id||r.id,name=r.employee_name||r.name||id,val=parseFloat(r.value)||0;if(!id)return;if(!DATA[id]){DATA[id]={id,name};SVCS.forEach(s=>{DATA[id][s.key]=0;});}DATA[id][key]=val;if(name&&name!==id)DATA[id].name=name;});}
function readFile(f,cb){const r=new FileReader();r.onload=e=>cb(e.target.result);r.readAsText(f);}
function updateMode(){const any=[...document.querySelectorAll('input[type=file]')].some(i=>i.files&&i.files.length);const pill=document.getElementById('modePill'),dot=pill.querySelector('.s-dot'),hdr=document.getElementById('hdrMode');if(any){dot.classList.remove('off');pill.lastChild.textContent='CSV Data';hdr.textContent='CSV DATA';}else{dot.classList.add('off');pill.lastChild.textContent='Mock Data';hdr.textContent='MOCK DATA';}}
document.getElementById('up_master').addEventListener('change',function(){if(!this.files[0])return;readFile(this.files[0],t=>{applyMaster(parseCSV(t));updateMode();renderAll();});});
SVCS.forEach(s=>{const el=document.getElementById('up_'+s.key);if(!el)return;el.addEventListener('change',function(){if(!this.files[0])return;readFile(this.files[0],t=>{applySvc(s.key,parseCSV(t));updateMode();renderAll();});});});
/* ═══════════════════════════
TABS
═══════════════════════════ */
document.querySelectorAll('.tab-btn').forEach(btn=>{btn.addEventListener('click',()=>{document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));document.querySelectorAll('.tab-pane').forEach(p=>p.classList.remove('active'));btn.classList.add('active');document.getElementById(btn.dataset.tab).classList.add('active');});});
document.querySelectorAll('.st-btn').forEach(btn=>{btn.addEventListener('click',()=>{document.querySelectorAll('.st-btn').forEach(b=>b.classList.remove('active'));document.querySelectorAll('.sub-panel').forEach(p=>p.classList.remove('active'));btn.classList.add('active');document.getElementById(btn.dataset.panel).classList.add('active');});});
document.getElementById('upHead').addEventListener('click',()=>{const b=document.getElementById('upBody'),a=document.getElementById('upArrow'),h=document.getElementById('upHead');const o=b.classList.toggle('open');a.classList.toggle('open',o);h.classList.toggle('open',o);});
/* ═══════════════════════════════════════════════
PDF EXPORT — WHITE THEME
═══════════════════════════════════════════════ */
async function exportPDF(){
showOverlay('Generating PDF Report','Building white-theme report...','expOverlay','expMsg','expSub');
await new Promise(r=>setTimeout(r,200));
try{
const {jsPDF}=window.jspdf;
const pdf=new jsPDF('l','mm','a4');
const pw=297,ph=210;
// ── WHITE PALETTE ──
const W=[255,255,255], LIGHT=[248,249,252], LIGHT2=[237,240,247];
const INK=[30,30,45], // near-black for body text
MUTED=[110,115,140], // secondary text / labels
RULE=[210,213,225]; // divider lines / borders
// accent colours — vivid on white
const AC_BLUE=[0,102,204], AC_BLUE_H='0066CC';
const AC_TEAL=[0,168,180], AC_TEAL_H='00A8B4';
const AC_GREEN=[16,160,90], AC_GREEN_H='10A05A';
const AC_ORANGE=[220,90,20], AC_ORANGE_H='DC5A14';
const AC_PURPLE=[100,55,200],AC_PURPLE_H='6437C8';
const AC_RED=[210,40,80], AC_RED_H='D22850';
// per-employee accent cycling (readable on white)
const EAC=[AC_BLUE_H,'D22850','6437C8','10A05A','DC5A14','C49000','008B8B'];
const EAC_RGB=[[0,102,204],[210,40,80],[100,55,200],[16,160,90],[220,90,20],[196,144,0],[0,139,139]];
const arr=EA(),gt=GT(),sorted=[...arr].sort((a,b)=>eTotal(b)-eTotal(a));
// helper: draw page chrome
const pageChrome=(acRGB,title,sub)=>{
pdf.setFillColor(...W); pdf.rect(0,0,pw,ph,'F');
// top accent bar (6 mm)
pdf.setFillColor(...acRGB); pdf.rect(0,0,pw,6,'F');
// bottom rule
pdf.setDrawColor(...RULE); pdf.setLineWidth(.3); pdf.line(0,ph-8,pw,ph-8);
pdf.setTextColor(...MUTED); pdf.setFontSize(7); pdf.setFont('helvetica','normal');
pdf.text('HR Performance Analytics · Confidential',14,ph-3.5);
pdf.text(`Page ${pdf.getCurrentPageInfo().pageNumber}`,pw-14,ph-3.5,{align:'right'});
if(title){
pdf.setTextColor(...INK); pdf.setFontSize(15); pdf.setFont('helvetica','bold');
pdf.text(title,14,16);
}
if(sub){
pdf.setTextColor(...MUTED); pdf.setFontSize(8.5); pdf.setFont('helvetica','normal');
pdf.text(sub,14,23);
}
};
// ── COVER PAGE ──
pdf.setFillColor(...W); pdf.rect(0,0,pw,ph,'F');
// left accent strip
pdf.setFillColor(...AC_BLUE); pdf.rect(0,0,8,ph,'F');
// header band
pdf.setFillColor(...LIGHT); pdf.rect(8,0,pw-8,50,'F');
pdf.setTextColor(...AC_BLUE); pdf.setFontSize(9); pdf.setFont('helvetica','bold');
pdf.text('HR PERFORMANCE ANALYTICS',pw/2+4,18,{align:'center'});
pdf.setDrawColor(...RULE); pdf.setLineWidth(.4); pdf.line(40,22,pw-20,22);
pdf.setTextColor(...INK); pdf.setFontSize(30); pdf.setFont('helvetica','bold');
pdf.text('Performance Dashboard',pw/2+4,40,{align:'center'});
// main area
pdf.setTextColor(...MUTED); pdf.setFontSize(11); pdf.setFont('helvetica','normal');
pdf.text(`Generated: ${new Date().toLocaleDateString('en-GB',{weekday:'long',year:'numeric',month:'long',day:'numeric'})}`,pw/2+4,60,{align:'center'});
pdf.setDrawColor(...RULE); pdf.setLineWidth(.4); pdf.line(50,65,pw-40,65);
pdf.setTextColor(...MUTED); pdf.setFontSize(9);
pdf.text(`${arr.length} Employees · ${SVCS.length} Services · Grand Total: ${gt}`,pw/2+4,73,{align:'center'});
// KPI summary boxes
const boxes=[
{l:'Grand Total',v:String(gt),rgb:AC_BLUE},
{l:'Team Average',v:(gt/(arr.length*SVCS.length)).toFixed(1),rgb:AC_TEAL},
{l:'Top Performer',v:sorted[0].name.split(' ')[0],rgb:AC_GREEN},
{l:'Efficiency',v:((gt/(arr.length*SVCS.length*100))*100).toFixed(1)+'%',rgb:AC_ORANGE},
];
boxes.forEach((b,i)=>{
const x=30+i*62, y=84;
pdf.setFillColor(...LIGHT); pdf.roundedRect(x,y,55,32,2,2,'F');
pdf.setDrawColor(...b.rgb); pdf.setLineWidth(.8); pdf.roundedRect(x,y,55,32,2,2,'S');
// top accent
pdf.setFillColor(...b.rgb); pdf.roundedRect(x,y,55,4,2,2,'F');
pdf.rect(x,y+2,55,2,'F');
pdf.setTextColor(...MUTED); pdf.setFontSize(6.5); pdf.setFont('helvetica','normal');
pdf.text(b.l,x+27.5,y+11,{align:'center'});
pdf.setTextColor(...b.rgb); pdf.setFontSize(17); pdf.setFont('helvetica','bold');
pdf.text(String(b.v),x+27.5,y+24,{align:'center'});
});
// leaderboard preview strip
pdf.setFillColor(...LIGHT2); pdf.roundedRect(14,125,pw-28,62,2,2,'F');
pdf.setTextColor(...INK); pdf.setFontSize(8); pdf.setFont('helvetica','bold');
pdf.text('Quick Rankings',20,133);
sorted.slice(0,5).forEach((emp,i)=>{
const rx=20, ry=138+i*9;
const rgb=EAC_RGB[EA().indexOf(emp)%EAC_RGB.length];
pdf.setFillColor(...rgb); pdf.roundedRect(rx,ry-4,1.8,6,0,0,'F');
const medal=i===0?'1st':i===1?'2nd':i===2?'3rd':`#${i+1}`;
pdf.setTextColor(...MUTED); pdf.setFontSize(6.5); pdf.setFont('helvetica','normal'); pdf.text(medal,rx+4,ry);
pdf.setTextColor(...INK); pdf.text(emp.name,rx+16,ry);
const bx=rx+90, bw=150, pct=eTotal(emp)/(eTotal(sorted[0])||1);
pdf.setFillColor(...RULE); pdf.roundedRect(bx,ry-3,bw,3.5,.5,.5,'F');
pdf.setFillColor(...rgb); pdf.roundedRect(bx,ry-3,bw*pct,3.5,.5,.5,'F');
pdf.setTextColor(...rgb); pdf.setFont('helvetica','bold'); pdf.text(String(eTotal(emp)),bx+bw+3,ry);
});
// bottom
pdf.setDrawColor(...RULE); pdf.setLineWidth(.3); pdf.line(14,ph-10,pw-14,ph-10);
pdf.setTextColor(...MUTED); pdf.setFontSize(7); pdf.setFont('helvetica','normal');
pdf.text('HR Performance Analytics · Confidential',14,ph-4.5);
// ── PAGE 2: EMPLOYEE SUMMARY TABLE ──
pdf.addPage();
pageChrome(AC_BLUE,'Employee Summary','Performance totals, averages and rankings across all services');
const cols=['Rank','Employee','ID',...SVCS.map(s=>s.label.slice(0,9)),'Total','Avg'];
const colW=[10,36,17,...SVCS.map(()=>16),16,13];
const startX=14, headerY=30;
// header
pdf.setFillColor(...AC_BLUE); pdf.rect(startX,headerY,pw-28,7,'F');
pdf.setTextColor(...W); pdf.setFontSize(6); pdf.setFont('helvetica','bold');
let cx=startX;
cols.forEach((c,i)=>{pdf.text(c,cx+colW[i]/2,headerY+4.8,{align:'center'});cx+=colW[i];});
// rows
sorted.forEach((emp,ri)=>{
const rowY=headerY+7+ri*9;
pdf.setFillColor(...(ri%2===0?LIGHT:W)); pdf.rect(startX,rowY,pw-28,9,'F');
pdf.setDrawColor(...RULE); pdf.setLineWidth(.15); pdf.rect(startX,rowY,pw-28,9,'S');
pdf.setFont('helvetica','normal'); pdf.setFontSize(6.5);
const cells=[`#${ri+1}`,emp.name,emp.id,...SVCS.map(s=>String(emp[s.key]||0)),String(eTotal(emp)),eAvg(emp).toFixed(1)];
cx=startX;
cells.forEach((cell,ci)=>{
if(ci===0){
const mc=ri===0?[180,140,0]:ri===1?[100,100,100]:ri===2?[160,80,20]:MUTED;
pdf.setTextColor(...mc); pdf.setFont('helvetica','bold');
} else if(ci===cols.length-2){pdf.setTextColor(...AC_BLUE); pdf.setFont('helvetica','bold');}
else if(ci===cols.length-1){pdf.setTextColor(...AC_GREEN); pdf.setFont('helvetica','bold');}
else if(ci>2){
const v=parseFloat(cell)||0;
pdf.setTextColor(...(v>=80?AC_GREEN:v>=60?AC_ORANGE:AC_RED));
pdf.setFont('helvetica','normal');
} else {pdf.setTextColor(...INK); pdf.setFont('helvetica','normal');}
pdf.text(cell,cx+colW[ci]/2,rowY+5.8,{align:'center'});
cx+=colW[ci];
});
});
// ── PAGES 3+: SERVICE BAR CHARTS ──
updateOverlayMsg('Rendering service charts...','expMsg');
for(let si=0;si<SVCS.length;si+=2){
pdf.addPage();
pageChrome(AC_TEAL,'Service Performance Comparison','Score distribution across all employees per service');
for(let ci=0;ci<2&&si+ci<SVCS.length;ci++){
const svc=SVCS[si+ci];
const emps=EA(), vals=emps.map(e=>e[svc.key]||0);
const t=sTotal(svc.key), a=sAvg(svc.key).toFixed(1);
const ox=14+ci*141, oy=30, barW=130, barH=145;
// card bg
pdf.setFillColor(...LIGHT); pdf.roundedRect(ox,oy,135,155,2,2,'F');
pdf.setDrawColor(...RULE); pdf.setLineWidth(.3); pdf.roundedRect(ox,oy,135,155,2,2,'S');
// card header strip
pdf.setFillColor(...AC_TEAL); pdf.roundedRect(ox,oy,135,8,2,2,'F');
pdf.rect(ox,oy+4,135,4,'F');
pdf.setTextColor(...W); pdf.setFontSize(8); pdf.setFont('helvetica','bold');
pdf.text(svc.label,ox+67.5,oy+5.5,{align:'center'});
// stats
pdf.setTextColor(...MUTED); pdf.setFontSize(6.5); pdf.setFont('helvetica','normal');
pdf.text(`Total: ${t}`,ox+4,oy+17); pdf.text(`Avg: ${a}`,ox+4,oy+24);
// bars
const barAreaX=ox+36, barAreaY=oy+30, maxV=Math.max(...vals,1);
const slot=barH/(emps.length+1);
emps.forEach((emp,ei)=>{
const v=vals[ei], pct=v/maxV;
const rgb=EAC_RGB[ei%EAC_RGB.length];
const by=barAreaY+slot*(ei+0.5), bh=Math.max(slot*.5,2.5), bw=pct*(barW-32);
// track
pdf.setFillColor(...RULE); pdf.roundedRect(barAreaX,by-bh/2,barW-32,bh,.5,.5,'F');
// filled bar
pdf.setFillColor(...rgb); pdf.roundedRect(barAreaX,by-bh/2,bw||0.5,bh,.5,.5,'F');
// label
pdf.setTextColor(...MUTED); pdf.setFontSize(5.8); pdf.setFont('helvetica','normal');
pdf.text(emp.name.split(' ')[0],barAreaX-2,by+1.5,{align:'right'});
// value
pdf.setTextColor(...rgb); pdf.setFontSize(6); pdf.setFont('helvetica','bold');
pdf.text(String(v),barAreaX+bw+2,by+1.5);
});
}
await new Promise(r=>setTimeout(r,0));
}
// ── LEADERBOARD PAGE ──
pdf.addPage();
pageChrome(AC_ORANGE,'Performance Leaderboard','Employees ranked by cumulative score across all services');
sorted.forEach((emp,i)=>{
const rowY=30+i*22, t=eTotal(emp), pct=t/(eTotal(sorted[0])||1);
const rgb=EAC_RGB[EA().indexOf(emp)%EAC_RGB.length];
pdf.setFillColor(...(i%2===0?LIGHT:W)); pdf.roundedRect(14,rowY,pw-28,18,1.5,1.5,'F');
pdf.setDrawColor(...RULE); pdf.setLineWidth(.25); pdf.roundedRect(14,rowY,pw-28,18,1.5,1.5,'S');
// left accent bar
pdf.setFillColor(...rgb); pdf.roundedRect(14,rowY,2.5,18,1,1,'F');
// rank
const medal=i===0?'1st':i===1?'2nd':i===2?'3rd':`#${i+1}`;
const mc=i===0?[180,140,0]:i===1?[100,100,100]:i===2?[160,80,20]:MUTED;
pdf.setTextColor(...mc); pdf.setFontSize(9.5); pdf.setFont('helvetica','bold');
pdf.text(medal,22,rowY+11);
// name
pdf.setTextColor(...INK); pdf.setFontSize(10); pdf.setFont('helvetica','bold');
pdf.text(emp.name,40,rowY+7);
// sub
pdf.setTextColor(...MUTED); pdf.setFontSize(7); pdf.setFont('helvetica','normal');
pdf.text(`Avg: ${eAvg(emp).toFixed(1)} · Best: ${eBest(emp).label} · Weakest: ${eWorst(emp).label}`,40,rowY+14);
// bar
const bx=135, by=rowY+6, bw=130, bh=5;
pdf.setFillColor(...RULE); pdf.roundedRect(bx,by,bw,bh,1,1,'F');
pdf.setFillColor(...rgb); pdf.roundedRect(bx,by,bw*pct,bh,1,1,'F');
pdf.setTextColor(...rgb); pdf.setFontSize(9); pdf.setFont('helvetica','bold');
pdf.text(String(t),bx+bw+4,rowY+11);
});
pdf.save('HR_Performance_Report.pdf');
hideOverlay('expOverlay');
}catch(err){console.error(err);hideOverlay('expOverlay');alert('PDF export failed: '+err.message);}
}
/* ═══════════════════════════════════════════════
PPTX EXPORT — WHITE THEME
═══════════════════════════════════════════════ */
async function exportPPTX(){
showOverlay('Generating PowerPoint','Building white-theme slides...','expOverlay','expMsg','expSub');
await new Promise(r=>setTimeout(r,200));
try{
const pres=new PptxGenJS();
pres.layout='LAYOUT_WIDE';
pres.title='HR Performance Dashboard';
pres.author='HR Analytics';
// ── WHITE PALETTE ──
const BG='FFFFFF', LIGHT='F5F7FA', LIGHT2='EDF0F7';
const INK='1E1E2D', // near-black body text
MUTED='6E7389', // secondary / labels
RULE='D2D5E1'; // borders / dividers
// vivid accents on white
const BLUE='0066CC', TEAL='0094A8', GREEN='0E8A52',
ORANGE='C85A10', PURPLE='5B2DBF', RED='C42050',
AMBER='B07800';
// per-employee colour cycling (readable on white)
const EAC=[BLUE,RED,PURPLE,GREEN,ORANGE,AMBER,'007C7C'];
const arr=EA(),gt=GT(),sorted=[...arr].sort((a,b)=>eTotal(b)-eTotal(a));
const empNames=arr.map(e=>e.name.split(' ')[0]);
// helper: standard page header
const slideHeader=(sl,acColor,title,sub)=>{
sl.background={color:BG};
// thin top accent bar
sl.addShape(pres.shapes.RECTANGLE,{x:0,y:0,w:13.3,h:.12,fill:{color:acColor}});
// footer rule
sl.addShape(pres.shapes.LINE,{x:0,y:7.33,w:13.3,h:0,line:{color:RULE,width:.5}});
sl.addText('HR Performance Analytics · Confidential',{x:.3,y:7.35,w:8,h:.15,fontSize:6.5,color:MUTED,fontFace:'Calibri'});
if(title) sl.addText(title,{x:.4,y:.2,w:10,h:.55,fontSize:20,bold:true,color:INK,fontFace:'Calibri'});
if(sub) sl.addText(sub,{x:.4,y:.74,w:12.5,h:.28,fontSize:9.5,color:MUTED,fontFace:'Calibri'});
};
// ── SLIDE 1: COVER ──
let sl=pres.addSlide();
sl.background={color:BG};
// left accent column
sl.addShape(pres.shapes.RECTANGLE,{x:0,y:0,w:.22,h:7.5,fill:{color:BLUE}});
// light header band
sl.addShape(pres.shapes.RECTANGLE,{x:.22,y:0,w:13.08,h:2.2,fill:{color:LIGHT}});
sl.addText('HR PERFORMANCE ANALYTICS',{x:.5,y:.35,w:12.5,h:.45,fontSize:11,bold:true,color:BLUE,charSpacing:4,fontFace:'Calibri'});
sl.addShape(pres.shapes.LINE,{x:.5,y:.88,w:12.4,h:0,line:{color:RULE,width:.6}});
sl.addText('Performance Dashboard',{x:.5,y:1.0,w:12.4,h:.9,fontSize:40,bold:true,color:INK,fontFace:'Calibri'});
sl.addText(`Generated ${new Date().toLocaleDateString('en-GB',{year:'numeric',month:'long',day:'numeric'})}`,{x:.5,y:2.35,w:12.4,h:.38,fontSize:12,color:MUTED,fontFace:'Calibri'});
sl.addShape(pres.shapes.LINE,{x:.5,y:2.78,w:12.4,h:0,line:{color:RULE,width:.5}});
// KPI boxes
const kpis=[
{l:'Grand Total',v:String(gt),c:BLUE},
{l:'Team Average',v:(gt/(arr.length*SVCS.length)).toFixed(1),c:TEAL},
{l:'Employees',v:String(arr.length),c:GREEN},
{l:'Services',v:String(SVCS.length),c:PURPLE},
];
kpis.forEach((k,i)=>{
const x=.5+i*3.2, y=3.05;
sl.addShape(pres.shapes.RECTANGLE,{x,y,w:3.0,h:1.6,fill:{color:LIGHT2},line:{color:RULE,width:.4}});
// top colour strip on box
sl.addShape(pres.shapes.RECTANGLE,{x,y,w:3.0,h:.12,fill:{color:k.c}});
sl.addText(k.l,{x,y:y+.18,w:3.0,h:.32,fontSize:8.5,color:MUTED,align:'center',fontFace:'Calibri'});
sl.addText(k.v,{x,y:y+.52,w:3.0,h:.82,fontSize:32,bold:true,color:k.c,align:'center',fontFace:'Calibri'});
});
// mini ranking strip
sl.addShape(pres.shapes.RECTANGLE,{x:.5,y:4.88,w:12.5,h:2.3,fill:{color:LIGHT}});
sl.addText('Quick Rankings',{x:.65,y:4.98,w:4,h:.32,fontSize:9,bold:true,color:INK,fontFace:'Calibri'});
sorted.slice(0,5).forEach((emp,i)=>{
const ei=arr.indexOf(emp), col=EAC[ei%EAC.length];
const rx=.65, ry=5.38+i*.31;
sl.addShape(pres.shapes.RECTANGLE,{x:rx,y:ry-.02,w:.07,h:.24,fill:{color:col}});
sl.addText(`${i===0?'1st':i===1?'2nd':i===2?'3rd':'#'+(i+1)} ${emp.name}`,{x:rx+.14,y:ry,w:4,h:.25,fontSize:8.5,color:INK,fontFace:'Calibri'});
// bar
const bx=5.0, bw=7.6, pct=eTotal(emp)/(eTotal(sorted[0])||1);
sl.addShape(pres.shapes.RECTANGLE,{x:bx,y:ry+.02,w:bw,h:.14,fill:{color:RULE}});
sl.addShape(pres.shapes.RECTANGLE,{x:bx,y:ry+.02,w:bw*pct,h:.14,fill:{color:col}});
sl.addText(String(eTotal(emp)),{x:bx+bw+.08,y:ry,w:.7,h:.25,fontSize:8.5,bold:true,color:col,fontFace:'Calibri'});
});
// ── SLIDE 2: EMPLOYEE SUMMARY TABLE ──
updateOverlayMsg('Building summary table...','expMsg');
sl=pres.addSlide();
slideHeader(sl,BLUE,'Employee Performance Summary',`Grand Total: ${gt} · ${arr.length} Employees · ${SVCS.length} Services`);
const tCols=['#','Name','ID',...SVCS.map(s=>s.label.slice(0,9)),'Total','Avg'];
const tW=[.32,.92,.62,...SVCS.map(()=>0.70),.63,.54];
const tHRows=[[tCols.map(c=>({text:c,options:{bold:true,color:'FFFFFF',fontSize:6.5,align:'center',fill:{color:BLUE},border:{pt:.5,color:RULE}}})) ]];
sorted.forEach((emp,ri)=>{
const row=[];
const rowFill=ri%2===0?BG:LIGHT;
row.push({text:`#${ri+1}`,options:{bold:true,color:ri===0?AMBER:ri===1?MUTED:ri===2?ORANGE:MUTED,fontSize:7,align:'center',fill:{color:rowFill},border:{pt:.3,color:RULE}}});
row.push({text:emp.name,options:{color:INK,fontSize:7,fill:{color:rowFill},border:{pt:.3,color:RULE}}});
row.push({text:emp.id,options:{color:MUTED,fontSize:6.5,align:'center',fill:{color:rowFill},border:{pt:.3,color:RULE}}});
SVCS.forEach(s=>{
const v=emp[s.key]||0;
row.push({text:String(v),options:{color:v>=80?GREEN:v>=60?ORANGE:RED,fontSize:7,align:'center',fill:{color:rowFill},border:{pt:.3,color:RULE}}});
});
row.push({text:String(eTotal(emp)),options:{bold:true,color:BLUE,fontSize:8,align:'center',fill:{color:rowFill},border:{pt:.3,color:RULE}}});
row.push({text:eAvg(emp).toFixed(1),options:{bold:true,color:GREEN,fontSize:7,align:'center',fill:{color:rowFill},border:{pt:.3,color:RULE}}});
tHRows.push(row);
});
sl.addTable(tHRows,{x:.3,y:1.1,w:12.7,colW:tW,rowH:.41,border:{pt:.3,color:RULE}});
// ── SLIDES 3+: SERVICE BAR CHARTS ──
updateOverlayMsg('Adding service charts...','expMsg');
for(let si=0;si<SVCS.length;si+=2){
sl=pres.addSlide();
slideHeader(sl,TEAL,'Service Performance Comparison','Employee scores per service — 0 to 100 scale');
for(let ci=0;ci<2&&si+ci<SVCS.length;ci++){
const svc=SVCS[si+ci];
const ox=.3+ci*6.65, oy=1.05, cw=6.2;
const vals=arr.map(e=>e[svc.key]||0);
// card bg
sl.addShape(pres.shapes.RECTANGLE,{x:ox,y:oy,w:cw,h:.52,fill:{color:LIGHT2},line:{color:RULE,width:.4}});
sl.addShape(pres.shapes.RECTANGLE,{x:ox,y:oy,w:.1,h:.52,fill:{color:TEAL}});
sl.addText(svc.label,{x:ox+.16,y:oy+.05,w:cw*.58,h:.42,fontSize:11,bold:true,color:INK,fontFace:'Calibri'});
sl.addText(`Total: ${sTotal(svc.key)} · Avg: ${sAvg(svc.key).toFixed(1)}`,{x:ox+cw*.5,y:oy+.12,w:cw*.48,h:.28,fontSize:8,color:MUTED,align:'right',fontFace:'Calibri'});
sl.addChart(pres.charts.BAR,[{name:svc.label,labels:empNames,values:vals}],{
x:ox, y:oy+.54, w:cw, h:5.6, barDir:'col',
chartColors:EAC,
chartArea:{fill:{color:BG},roundedCorners:false},
plotArea:{fill:{color:BG}},
catAxisLabelColor:MUTED, valAxisLabelColor:MUTED,
catAxisLineShow:false, valAxisLineShow:false,
valGridLine:{color:RULE,size:.4}, catGridLine:{style:'none'},
valAxisMinVal:0, valAxisMaxVal:100,
showValue:true, dataLabelColor:INK, dataLabelFontSize:7,
showLegend:false,
});
}
await new Promise(r=>setTimeout(r,0));
}
// ── SLIDES: DISTRIBUTION DOUGHNUT ──
updateOverlayMsg('Adding distribution charts...','expMsg');
for(let si=0;si<SVCS.length;si+=3){
sl=pres.addSlide();
slideHeader(sl,PURPLE,'Service Distribution','Share of total score per employee per service');
for(let ci=0;ci<3&&si+ci<SVCS.length;ci++){
const svc=SVCS[si+ci];
const ox=.25+ci*4.36, oy=1.05, cw=4.1;
const vals=arr.map(e=>e[svc.key]||0);
sl.addShape(pres.shapes.RECTANGLE,{x:ox,y:oy,w:cw,h:.46,fill:{color:LIGHT2},line:{color:RULE,width:.4}});
sl.addShape(pres.shapes.RECTANGLE,{x:ox,y:oy,w:.1,h:.46,fill:{color:PURPLE}});
sl.addText(svc.label,{x:ox+.14,y:oy+.07,w:cw-.22,h:.32,fontSize:9.5,bold:true,color:INK,fontFace:'Calibri'});
sl.addChart(pres.charts.DOUGHNUT,[{name:svc.label,labels:empNames,values:vals}],{
x:ox, y:oy+.48, w:cw, h:5.8,
holeSize:55,
chartColors:EAC,
chartArea:{fill:{color:BG}},
dataLabelFontSize:7, dataLabelColor:INK, showPercent:true,
legendPos:'b', showLegend:true, legendFontSize:7.5, legendColor:MUTED,
});
}
await new Promise(r=>setTimeout(r,0));
}
// ── SLIDE: LEADERBOARD ──
updateOverlayMsg('Adding leaderboard...','expMsg');
sl=pres.addSlide();
slideHeader(sl,ORANGE,'Performance Leaderboard','Employees ranked by cumulative score across all 11 services');
sl.addText(`Grand Total: ${gt}`,{x:9.5,y:.2,w:3.5,h:.55,fontSize:14,bold:true,color:BLUE,align:'right',fontFace:'Calibri'});
sorted.forEach((emp,i)=>{
const ry=1.12+i*.86, t=eTotal(emp), pct=t/(eTotal(sorted[0])||1);
const ei=arr.indexOf(emp), col=EAC[ei%EAC.length];
// row bg
sl.addShape(pres.shapes.RECTANGLE,{x:.3,y:ry,w:12.7,h:.72,fill:{color:i%2===0?LIGHT:BG},line:{color:RULE,width:.3}});
// left colour accent
sl.addShape(pres.shapes.RECTANGLE,{x:.3,y:ry,w:.12,h:.72,fill:{color:col}});
const medal=i===0?'🥇':i===1?'🥈':i===2?'🥉':`#${i+1}`;
sl.addText(medal,{x:.5,y:ry+.08,w:.62,h:.54,fontSize:15,align:'center',fontFace:'Segoe UI Emoji'});
sl.addText(emp.name,{x:1.22,y:ry+.05,w:4,h:.3,fontSize:11,bold:true,color:INK,fontFace:'Calibri'});
sl.addText(`Avg: ${eAvg(emp).toFixed(1)} · Best: ${eBest(emp).label} · Weakest: ${eWorst(emp).label}`,{x:1.22,y:ry+.38,w:5.2,h:.26,fontSize:7.5,color:MUTED,fontFace:'Calibri'});
// progress bar track
const bx=6.8, bw=5.5;
sl.addShape(pres.shapes.RECTANGLE,{x:bx,y:ry+.22,w:bw,h:.28,fill:{color:RULE}});
if(pct>0) sl.addShape(pres.shapes.RECTANGLE,{x:bx,y:ry+.22,w:bw*pct,h:.28,fill:{color:col}});
sl.addText(String(t),{x:bx+bw+.1,y:ry+.14,w:.82,h:.44,fontSize:14,bold:true,color:col,fontFace:'Calibri'});
});
// ── SLIDE: INSIGHTS — TOTAL + GAP ──
updateOverlayMsg('Building insight charts...','expMsg');
sl=pres.addSlide();
slideHeader(sl,GREEN,'Performance Insights','Total scores and performance gap analysis across employees and services');
sl.addShape(pres.shapes.RECTANGLE,{x:.3,y:1.0,w:6.1,h:.42,fill:{color:LIGHT2},line:{color:RULE,width:.3}});
sl.addShape(pres.shapes.RECTANGLE,{x:.3,y:1.0,w:.1,h:.42,fill:{color:BLUE}});
sl.addText('Total Score per Employee',{x:.46,y:1.07,w:5.9,h:.3,fontSize:9.5,bold:true,color:INK,fontFace:'Calibri'});
sl.addChart(pres.charts.BAR,[{name:'Total',labels:sorted.map(e=>e.name.split(' ')[0]),values:sorted.map(e=>eTotal(e))}],{
x:.3,y:1.44,w:6.1,h:5.8,barDir:'col',
chartColors:sorted.map(e=>EAC[arr.indexOf(e)%EAC.length]),
chartArea:{fill:{color:BG}}, catAxisLabelColor:MUTED, valAxisLabelColor:MUTED,
valGridLine:{color:RULE,size:.4}, catGridLine:{style:'none'},
showValue:true, dataLabelColor:INK, dataLabelFontSize:8, showLegend:false,
});
const gaps=SVCS.map(s=>{const v=arr.map(e=>e[s.key]||0);return Math.max(...v)-Math.min(...v);});
sl.addShape(pres.shapes.RECTANGLE,{x:6.9,y:1.0,w:6.1,h:.42,fill:{color:LIGHT2},line:{color:RULE,width:.3}});
sl.addShape(pres.shapes.RECTANGLE,{x:6.9,y:1.0,w:.1,h:.42,fill:{color:ORANGE}});
sl.addText('Performance Gap per Service (Max − Min)',{x:7.06,y:1.07,w:5.9,h:.3,fontSize:9.5,bold:true,color:INK,fontFace:'Calibri'});
sl.addChart(pres.charts.BAR,[{name:'Gap',labels:SVCS.map(s=>s.label),values:gaps}],{
x:6.9,y:1.44,w:6.1,h:5.8,barDir:'bar',
chartColors:gaps.map(g=>g>40?RED:g>20?ORANGE:GREEN),
chartArea:{fill:{color:BG}}, catAxisLabelColor:MUTED, valAxisLabelColor:MUTED,
valGridLine:{color:RULE,size:.4}, catGridLine:{style:'none'},
showValue:true, dataLabelColor:INK, dataLabelFontSize:7.5, showLegend:false,
});
// ── SLIDE: TEAM ANALYTICS — AVG LINE + PIE ──
sl=pres.addSlide();
slideHeader(sl,TEAL,'Team Analytics','Average score per employee and service volume distribution');
sl.addShape(pres.shapes.RECTANGLE,{x:.3,y:1.0,w:6.1,h:.42,fill:{color:LIGHT2},line:{color:RULE,width:.3}});
sl.addShape(pres.shapes.RECTANGLE,{x:.3,y:1.0,w:.1,h:.42,fill:{color:TEAL}});
sl.addText('Average Score per Employee',{x:.46,y:1.07,w:5.9,h:.3,fontSize:9.5,bold:true,color:INK,fontFace:'Calibri'});
sl.addChart(pres.charts.LINE,[{name:'Avg Score',labels:arr.map(e=>e.name.split(' ')[0]),values:arr.map(e=>parseFloat(eAvg(e).toFixed(1)))}],{
x:.3,y:1.44,w:6.1,h:5.8,lineSize:2.5,lineSmooth:true,
chartColors:[BLUE],
chartArea:{fill:{color:BG}}, catAxisLabelColor:MUTED, valAxisLabelColor:MUTED,
valGridLine:{color:RULE,size:.4}, catGridLine:{style:'none'},
valAxisMinVal:0, valAxisMaxVal:100,
showValue:true, dataLabelColor:INK, dataLabelFontSize:8, showLegend:false,
});
sl.addShape(pres.shapes.RECTANGLE,{x:6.9,y:1.0,w:6.1,h:.42,fill:{color:LIGHT2},line:{color:RULE,width:.3}});
sl.addShape(pres.shapes.RECTANGLE,{x:6.9,y:1.0,w:.1,h:.42,fill:{color:PURPLE}});
sl.addText('Service Volume Distribution',{x:7.06,y:1.07,w:5.9,h:.3,fontSize:9.5,bold:true,color:INK,fontFace:'Calibri'});
sl.addChart(pres.charts.PIE,[{name:'Volume',labels:SVCS.map(s=>s.label),values:SVCS.map(s=>sTotal(s.key))}],{
x:6.9,y:1.44,w:6.1,h:5.8,
chartColors:EAC,
chartArea:{fill:{color:BG}},
showPercent:true, dataLabelColor:INK, dataLabelFontSize:7.5,
legendPos:'r', showLegend:true, legendFontSize:8, legendColor:MUTED,
});
await pres.writeFile({fileName:'HR_Performance_Dashboard.pptx'});
hideOverlay('expOverlay');
}catch(err){console.error(err);hideOverlay('expOverlay');alert('PPTX export failed: '+err.message);}
}
async function exportBoth(){await exportPDF();await new Promise(r=>setTimeout(r,300));await exportPPTX();}
/* helpers */
function showOverlay(msg,sub,oId,mId,sId){
document.getElementById(oId).classList.add('show');
document.getElementById(mId).textContent=msg;
document.getElementById(sId).textContent=sub;
}
function updateOverlayMsg(msg,id){document.getElementById(id).textContent=msg;}
function hideOverlay(id){document.getElementById(id).classList.remove('show');}
/* ═══ INIT ═══ */
renderInstrCards();
renderAll();
</script>
</body>
</html>
Performance Analytics
MOCK DATAv4.0
CSV Data Import
▼
KPI Overview
Employee Performance Cards
Service Totals
Service Comparison Charts
Distribution by Service
Performance Intelligence
🏆 Overall Ranking
📡Team Average by Service
Individual Radar Profiles
Comparative Insights
📋
CSV Format Guide
Upload a Master CSV (all 11 services) or individual service CSVs. Service files override master for that service. Headers are case-insensitive; spaces normalize to underscores.
master.csv — Column ReferenceMaster
| Column | Type | Req | Description |
|---|---|---|---|
| employee_id | string | Yes | Primary key — unique per employee, used for cross-file merging. |
| employee_name | string | Yes | Full display name shown in all cards and charts. |
| hiring | number | Yes | Score for Hiring service (0–100). |
| employee_status_update | number | Yes | Score for status change transactions. |
| advance_children_education | number | Yes | Advance on children's education benefit score. |
| education_settlement | number | Yes | Education reimbursement settlement score. |
| wages | number | Yes | Wage processing / payroll score. |
| work_schedule_ot | number | Yes | Work schedule and overtime approval score. |
| internal_transfer | number | Yes | Internal transfer requests score. |
| hop | number | Yes | HOP (Head of Personnel) transaction score. |
| separation | number | Yes | Separation / offboarding case score. |
| grievance | number | Yes | Grievance cases handled score. |
| investigation_cases | number | Yes | Investigation cases closed score. |
Sample — master.csvExample
CSV Preview
employee_id,employee_name,hiring,employee_status_update,advance_children_education,education_settlement,wages,work_schedule_ot,internal_transfer,hop,separation,grievance,investigation_cases EMP001,Ahmed Al-Rashidi,85,72,90,68,77,55,80,62,71,45,58 EMP002,Sara Binsaleh,91,88,74,82,95,70,65,79,83,60,72 EMP003,Omar Khalidi,67,55,80,73,60,85,90,50,66,77,88
Formatting NotesTips
Header row
Must be line 1. Auto-lowercased; spaces → underscores.
Delimiter
Comma only. No tabs or semicolons.
Values
Integers or decimals. Empty → 0.
Encoding
UTF-8 without BOM recommended.
Universal 3-Column Structure — all service CSVsAll Services
| Column | Type | Req | Description |
|---|---|---|---|
| employee_id | string | Yes | Unique employee ID. Must match existing data for correct merging. |
| employee_name | string | Opt | Full name. Updates display if provided; preserved if omitted. |
| value | number | Yes | Score for this specific service. Always named value. |
📥
Master CSV Priority
Populates all 11 services for all employees at once. Baseline dataset.
🔄
Service Override
Individual CSV overrides only that service. Others remain unchanged.
🆕
Auto-Create Employee
Unknown employee_id in service CSV creates employee with 0s elsewhere.
🎭
Mock Fallback
No CSV? Randomized mock data for 7 employees across 11 services.
🔁
Auto Re-render
All tabs, charts, totals, PDF/PPTX use fresh data on every upload.
⚠️
Empty → 0
Non-numeric or empty cells silently default to 0.
💡
Recommended: Use master.csv for a full data load, then individual service CSVs for incremental updates. The Export PDF and Export PPTX buttons always reflect the current data state.
Comments
Post a Comment