<!DOCTYPE html>
<!-- ============================================================
HR PERFORMANCE DASHBOARD v11
White Theme | Manual Entry | 6 Ranked Employees + 1 Specialist
14 Services | PDF + PPTX Export
============================================================
HOW TO EDIT IN NOTEPAD:
1. Find the section you want by searching (Ctrl+F) the label
e.g. "BLOCK: EMPLOYEES CONFIG" to change employee names/IDs
e.g. "BLOCK: SERVICES CONFIG" to change service names
e.g. "BLOCK: SPECIALIST CONFIG" to change specialist details
2. Each block is clearly marked with START and END comments
3. Save the file and refresh in your browser
============================================================ -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HR Performance Dashboard</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/pptxgenjs@3.12.0/dist/pptxgen.bundle.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=IBM+Plex+Mono:wght@400;600&display=swap" rel="stylesheet">
<!-- ============================================================
BLOCK: CSS VARIABLES (colours, spacing)
Change colour values here to retheme the dashboard
============================================================ -->
<style>
:root {
/* --- Background & Surfaces --- */
--bg: #F0F3F7;
--surface: #FFFFFF;
--surface2: #F4F6FA;
--surface3: #E8EDF5;
/* --- Borders --- */
--border: #DDE3EC;
--border2: #C4CDD9;
/* --- Text --- */
--ink: #0D1825;
--body: #2C3A4E;
--muted: #68788E;
/* --- Green Scale (primary accent) --- */
--g1: #0A5C38;
--g2: #138050;
--g3: #1DAD6C;
--g4: #5EDCA8;
--g5: #C2F0DC;
--g6: #EBF9F3;
/* --- Secondary Accents --- */
--blue: #1A5CB4;
--purple: #6836D0;
--orange: #D95B0A;
--amber: #B57D00;
--red: #C43030;
--teal: #0E8A8A;
/* --- Specialist Employee Accent --- */
--spec: #7C3AED;
--spec2: #F0EAFF;
--spec3: #DDD0FF;
}
/* ============================================================
END BLOCK: CSS VARIABLES
============================================================ */
/* ============================================================
BLOCK: BASE STYLES
============================================================ */
* { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 15px; }
body {
background: var(--bg);
color: var(--body);
font-family: 'Inter', sans-serif;
font-size: 14px;
line-height: 1.6;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: var(--surface2); }
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
/* ============================================================
END BLOCK: BASE STYLES
============================================================ */
/* ============================================================
BLOCK: HEADER STYLES
============================================================ */
header {
background: var(--g1);
padding: 0 28px;
display: flex; align-items: center; justify-content: space-between;
height: 62px;
position: sticky; top: 0; z-index: 300;
box-shadow: 0 2px 12px rgba(10,92,56,.25);
}
.logo { display: flex; align-items: center; gap: 12px; }
.logo-icon {
width: 38px; height: 38px;
background: rgba(255,255,255,.18);
border: 1.5px solid rgba(255,255,255,.35);
border-radius: 9px;
display: flex; align-items: center; justify-content: center;
font-family: 'IBM Plex Mono', monospace; font-size: 13px; font-weight: 700; color: #fff;
}
.logo-main { font-size: 15px; font-weight: 700; color: #fff; }
.logo-sub { font-size: 10px; color: rgba(255,255,255,.6); letter-spacing: 1.2px; text-transform: uppercase; margin-top: 1px; }
.hdr-right { display: flex; align-items: center; gap: 9px; }
.hdr-badge {
background: rgba(255,255,255,.14); border: 1px solid rgba(255,255,255,.28);
border-radius: 20px; padding: 4px 13px;
font-family: 'IBM Plex Mono', monospace; font-size: 9px;
color: rgba(255,255,255,.75); letter-spacing: 1px; text-transform: uppercase;
}
.exp-btn {
display: flex; align-items: center; gap: 6px; padding: 7px 14px;
border-radius: 7px; font-size: 12px; font-weight: 600; cursor: pointer;
border: 1.5px solid; transition: .18s; white-space: nowrap;
}
.exp-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,.18); }
.exp-btn.pdf { background: rgba(255,255,255,.92); border-color: transparent; color: var(--g1); }
.exp-btn.pptx { background: rgba(255,255,255,.14); border-color: rgba(255,255,255,.4); color: #fff; }
.exp-btn.all { background: var(--g4); border-color: var(--g4); color: var(--g1); }
/* ============================================================
END BLOCK: HEADER STYLES
============================================================ */
/* ============================================================
BLOCK: EXPORT OVERLAY STYLES
============================================================ */
.exp-overlay {
display: none; position: fixed; inset: 0; z-index: 9999;
background: rgba(13,24,37,.6); backdrop-filter: blur(7px);
align-items: center; justify-content: center;
flex-direction: column; gap: 16px;
}
.exp-overlay.show { display: flex; }
.exp-spin {
width: 46px; height: 46px;
border: 3px solid rgba(255,255,255,.2);
border-top-color: var(--g3);
border-radius: 50%; animation: spin .75s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.exp-msg { font-size: 15px; font-weight: 700; color: #fff; }
.exp-sub { font-size: 12px; color: rgba(255,255,255,.6); }
/* ============================================================
END BLOCK: EXPORT OVERLAY STYLES
============================================================ */
/* ============================================================
BLOCK: STATUS BAR STYLES
============================================================ */
.status-bar {
display: flex; gap: 8px; padding: 14px 28px 0; flex-wrap: wrap;
}
.s-pill {
background: var(--surface); border: 1px solid var(--border);
border-radius: 20px; padding: 5px 13px;
font-size: 12px; font-weight: 500; color: var(--muted);
display: flex; align-items: center; gap: 6px;
box-shadow: 0 1px 3px rgba(0,0,0,.05);
}
.s-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--g3); box-shadow: 0 0 0 2px var(--g6); }
.s-dot.spec { background: var(--spec); box-shadow: 0 0 0 2px var(--spec2); }
/* ============================================================
END BLOCK: STATUS BAR STYLES
============================================================ */
/* ============================================================
BLOCK: TAB NAVIGATION STYLES
============================================================ */
.tab-bar {
display: flex; padding: 0 28px;
background: var(--surface);
border-bottom: 2px solid var(--border);
margin-top: 14px; overflow-x: auto; gap: 2px;
}
.tab-btn {
padding: 13px 20px; font-size: 13px; font-weight: 600; color: var(--muted);
background: transparent; border: none; cursor: pointer;
position: relative; transition: .2s; white-space: nowrap; flex-shrink: 0;
}
.tab-btn::after {
content: ''; position: absolute; bottom: -2px; left: 0; right: 0;
height: 2px; background: var(--g1); transform: scaleX(0); transition: .2s;
}
.tab-btn:hover { color: var(--ink); }
.tab-btn.active { color: var(--g1); }
.tab-btn.active::after { transform: scaleX(1); }
/* per-tab active accent overrides */
.tab-btn[data-tab="tp-insights"].active { color: var(--g1); }
.tab-btn[data-tab="tp-emp"].active { color: var(--blue); }
.tab-btn[data-tab="tp-emp"].active::after { background: var(--blue); }
.tab-btn[data-tab="tp-svc"].active { color: var(--purple); }
.tab-btn[data-tab="tp-svc"].active::after { background: var(--purple); }
.tab-btn[data-tab="tp-dist"].active { color: var(--teal); }
.tab-btn[data-tab="tp-dist"].active::after { background: var(--teal); }
.tab-btn[data-tab="tp-other"].active { color: var(--spec); }
.tab-btn[data-tab="tp-other"].active::after { background: var(--spec); }
.tab-btn[data-tab="tp-entry"].active { color: var(--orange); }
.tab-btn[data-tab="tp-entry"].active::after { background: var(--orange); }
.tab-pane { display: none; padding: 24px 28px; }
.tab-pane.active { display: block; }
/* ============================================================
END BLOCK: TAB NAVIGATION STYLES
============================================================ */
/* ============================================================
BLOCK: SECTION TITLE STYLES
============================================================ */
.sec-t {
font-size: 10px; font-weight: 700; letter-spacing: 2.5px;
text-transform: uppercase; color: var(--muted);
margin-bottom: 14px;
display: flex; align-items: center; gap: 10px;
}
.sec-t::after { content: ''; flex: 1; height: 1px; background: var(--border); }
/* ============================================================
END BLOCK: SECTION TITLE STYLES
============================================================ */
/* ============================================================
BLOCK: KPI CARD STYLES
============================================================ */
.kpi-strip { display: grid; grid-template-columns: repeat(auto-fill, minmax(155px,1fr)); gap: 10px; margin-bottom: 22px; }
.kpi-card {
background: var(--surface); border: 1px solid var(--border);
border-radius: 11px; padding: 15px 17px;
position: relative; overflow: hidden; transition: .2s;
box-shadow: 0 1px 4px rgba(0,0,0,.05);
}
.kpi-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; }
.kpi-card.cg::before { background: var(--g1); }
.kpi-card.cb::before { background: var(--blue); }
.kpi-card.co::before { background: var(--orange); }
.kpi-card.cr::before { background: var(--red); }
.kpi-card.cp::before { background: var(--purple); }
.kpi-card.ct::before { background: var(--teal); }
.kpi-card:hover { box-shadow: 0 4px 14px rgba(0,0,0,.1); transform: translateY(-1px); }
.kpi-lbl { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .9px; color: var(--muted); }
.kpi-val { font-size: 24px; font-weight: 800; color: var(--ink); line-height: 1.1; margin: 4px 0 3px; }
.kpi-sub { font-size: 11px; color: var(--muted); }
.kpi-ico { position: absolute; right: 13px; top: 13px; font-size: 18px; opacity: .14; }
/* ============================================================
END BLOCK: KPI CARD STYLES
============================================================ */
/* ============================================================
BLOCK: INSIGHT CARD STYLES
============================================================ */
.ins-kpi { display: grid; grid-template-columns: repeat(auto-fill, minmax(195px,1fr)); gap: 10px; margin-bottom: 20px; }
.ik {
background: var(--surface); border: 1px solid var(--border);
border-radius: 11px; padding: 15px 17px;
display: flex; gap: 13px; align-items: center;
box-shadow: 0 1px 4px rgba(0,0,0,.05); transition: .2s;
position: relative; overflow: hidden;
}
.ik::before { content: ''; position: absolute; top: 0; left: 0; bottom: 0; width: 4px; }
.ik.green::before { background: var(--g1); }
.ik.blue::before { background: var(--blue); }
.ik.orange::before { background: var(--orange); }
.ik.red::before { background: var(--red); }
.ik.purple::before { background: var(--purple); }
.ik.teal::before { background: var(--teal); }
.ik:hover { box-shadow: 0 4px 14px rgba(0,0,0,.09); }
.ik-ico { font-size: 20px; flex-shrink: 0; }
.ik-lbl { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .8px; color: var(--muted); margin-bottom: 2px; }
.ik-val { font-size: 20px; font-weight: 800; color: var(--ink); }
.ik-sub { font-size: 11px; color: var(--muted); margin-top: 1px; }
/* ============================================================
END BLOCK: INSIGHT CARD STYLES
============================================================ */
/* ============================================================
BLOCK: CHART CARD STYLES
============================================================ */
.chart-card {
background: var(--surface); border: 1px solid var(--border);
border-radius: 12px; padding: 18px; transition: .2s;
box-shadow: 0 1px 4px rgba(0,0,0,.05);
}
.chart-card:hover { box-shadow: 0 5px 16px rgba(0,0,0,.09); }
.cc-title {
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px;
color: var(--g1); margin-bottom: 13px; padding-bottom: 10px;
border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 8px;
}
.cc-title span { font-size: 14px; }
.cc-ch { position: relative; height: 220px; }
/* ============================================================
END BLOCK: CHART CARD STYLES
============================================================ */
/* ============================================================
BLOCK: LEADERBOARD STYLES
============================================================ */
.lb-wrap { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 18px; box-shadow: 0 1px 4px rgba(0,0,0,.05); }
.lb-title { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: var(--amber); margin-bottom: 13px; padding-bottom: 10px; border-bottom: 1px solid var(--border); }
.lb-row { display: flex; align-items: center; gap: 11px; padding: 9px 0; border-bottom: 1px solid var(--border); }
.lb-row:last-child { border-bottom: none; }
.lb-rank { font-family: 'IBM Plex Mono', monospace; font-size: 12px; font-weight: 700; width: 24px; text-align: center; flex-shrink: 0; }
.lb-rank.r1 { color: #9A7000; }
.lb-rank.r2 { color: #707070; }
.lb-rank.r3 { color: #8B5A2B; }
.lb-rank.rn { color: var(--muted); }
.lb-av { width: 31px; height: 31px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-family: 'IBM Plex Mono', monospace; font-size: 11px; font-weight: 700; color: #fff; flex-shrink: 0; }
.lb-info { flex: 1; min-width: 0; }
.lb-name { font-size: 13px; font-weight: 600; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-sub { font-size: 11px; color: var(--muted); }
.lb-bar-wrap { flex: 1; background: var(--surface2); border-radius: 4px; height: 6px; max-width: 110px; }
.lb-bar { height: 100%; border-radius: 4px; }
.lb-score { font-family: 'IBM Plex Mono', monospace; font-size: 13px; font-weight: 700; min-width: 34px; text-align: right; }
/* ============================================================
END BLOCK: LEADERBOARD STYLES
============================================================ */
/* ============================================================
BLOCK: EMPLOYEE CARD STYLES
============================================================ */
.emp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(360px,1fr)); gap: 14px; }
.emp-card {
background: var(--surface); border: 1px solid var(--border);
border-radius: 12px; padding: 18px; transition: .2s;
box-shadow: 0 1px 4px rgba(0,0,0,.05);
position: relative; overflow: hidden;
}
.emp-card::before {
content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
background: linear-gradient(90deg, var(--g1), var(--blue));
opacity: 0; transition: .2s;
}
.emp-card:hover { box-shadow: 0 6px 18px rgba(0,0,0,.1); transform: translateY(-2px); border-color: var(--g5); }
.emp-card:hover::before { opacity: 1; }
.emp-head { display: flex; align-items: center; gap: 11px; margin-bottom: 11px; }
.emp-av { width: 42px; height: 42px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-family: 'IBM Plex Mono', monospace; font-size: 13px; font-weight: 700; flex-shrink: 0; color: #fff; }
.emp-nm { font-size: 14px; font-weight: 700; color: var(--ink); }
.emp-id { font-family: 'IBM Plex Mono', monospace; font-size: 9px; color: var(--muted); margin-top: 1px; }
.emp-badges { display: flex; gap: 7px; margin-bottom: 12px; flex-wrap: wrap; }
.e-badge { background: var(--surface2); border: 1px solid var(--border); border-radius: 7px; padding: 5px 10px; font-size: 11px; display: flex; flex-direction: column; gap: 1px; }
.e-badge .bl { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .7px; color: var(--muted); }
.e-badge .bv { font-size: 14px; font-weight: 700; }
.e-badge.tot .bv { color: var(--g1); }
.e-badge.avg .bv { color: var(--blue); }
.e-badge.best .bv { color: var(--amber); font-size: 11px; }
.e-badge.weak .bv { color: var(--red); font-size: 11px; }
.emp-ch { position: relative; height: 225px; }
/* ============================================================
END BLOCK: EMPLOYEE CARD STYLES
============================================================ */
/* ============================================================
BLOCK: SERVICE TAB STYLES
============================================================ */
.svc-tot-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(155px,1fr)); gap: 8px; margin-bottom: 20px; }
.stc { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 13px 14px; transition: .2s; box-shadow: 0 1px 3px rgba(0,0,0,.04); }
.stc:hover { box-shadow: 0 4px 12px rgba(0,0,0,.09); }
.stc-lbl { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .8px; color: var(--muted); margin-bottom: 3px; }
.stc-val { font-size: 21px; font-weight: 800; color: var(--g1); }
.stc-avg { font-size: 11px; color: var(--muted); margin-top: 1px; }
.svc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px,1fr)); gap: 14px; }
.svc-card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 17px; transition: .2s; box-shadow: 0 1px 4px rgba(0,0,0,.05); }
.svc-card:hover { box-shadow: 0 5px 16px rgba(0,0,0,.09); }
.svc-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 11px; padding-bottom: 10px; border-bottom: 1px solid var(--border); }
.svc-name { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--g1); }
.svc-stats { display: flex; gap: 12px; }
.svc-stat { font-size: 10px; font-weight: 600; color: var(--muted); display: flex; flex-direction: column; align-items: flex-end; }
.svc-stat strong { font-size: 13px; font-weight: 700; color: var(--ink); }
.svc-ch { position: relative; height: 170px; }
/* ============================================================
END BLOCK: SERVICE TAB STYLES
============================================================ */
/* ============================================================
BLOCK: DISTRIBUTION TAB STYLES
============================================================ */
.dist-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(265px,1fr)); gap: 14px; }
.dist-card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 17px; transition: .2s; box-shadow: 0 1px 4px rgba(0,0,0,.05); }
.dist-card:hover { box-shadow: 0 5px 16px rgba(0,0,0,.09); }
.dist-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 11px; padding-bottom: 10px; border-bottom: 1px solid var(--border); }
.dist-name { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--purple); }
.dist-tot { font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: var(--muted); background: var(--surface2); padding: 3px 9px; border-radius: 5px; }
.dist-ch { position: relative; height: 200px; }
/* ============================================================
END BLOCK: DISTRIBUTION TAB STYLES
============================================================ */
/* ============================================================
BLOCK: RADAR CHART STYLES
============================================================ */
.radar-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(258px,1fr)); gap: 14px; }
.radar-card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 15px; transition: .2s; box-shadow: 0 1px 4px rgba(0,0,0,.05); }
.radar-card:hover { box-shadow: 0 5px 14px rgba(0,0,0,.09); }
.radar-head { display: flex; align-items: center; gap: 9px; margin-bottom: 11px; }
.radar-av { width: 32px; height: 32px; border-radius: 7px; display: flex; align-items: center; justify-content: center; font-family: 'IBM Plex Mono', monospace; font-size: 11px; font-weight: 700; color: #fff; flex-shrink: 0; }
.radar-nm { font-size: 13px; font-weight: 700; color: var(--ink); }
.radar-sc { font-size: 11px; color: var(--muted); }
.radar-ch { position: relative; height: 195px; }
/* ============================================================
END BLOCK: RADAR CHART STYLES
============================================================ */
/* ============================================================
BLOCK: OTHER ACTIONS TAB STYLES (Specialist Employee)
============================================================ */
.oa-layout { display: grid; grid-template-columns: 1fr 2fr; gap: 20px; align-items: start; }
@media (max-width: 900px) { .oa-layout { grid-template-columns: 1fr; } }
.oa-total-card {
background: var(--surface); border: 1.5px solid var(--spec3);
border-radius: 14px; padding: 24px;
box-shadow: 0 2px 12px rgba(124,58,237,.1);
position: relative; overflow: hidden;
}
.oa-total-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: linear-gradient(90deg, var(--spec), var(--blue)); }
.oa-emp-badge { display: flex; align-items: center; gap: 10px; background: var(--spec2); border: 1px solid var(--spec3); border-radius: 9px; padding: 10px 14px; margin-bottom: 18px; }
.oa-emp-av { width: 36px; height: 36px; border-radius: 8px; background: linear-gradient(135deg, var(--spec), var(--blue)); display: flex; align-items: center; justify-content: center; font-family: 'IBM Plex Mono', monospace; font-size: 12px; font-weight: 700; color: #fff; flex-shrink: 0; }
.oa-emp-name { font-size: 13px; font-weight: 700; color: var(--ink); }
.oa-emp-sub { font-size: 10px; color: var(--muted); }
.oa-big-stat { text-align: center; padding: 18px 0; border-top: 1px solid var(--spec3); border-bottom: 1px solid var(--spec3); margin-bottom: 16px; }
.oa-big-lbl { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); margin-bottom: 6px; }
.oa-big-val { font-size: 56px; font-weight: 800; color: var(--spec); font-family: 'IBM Plex Mono', monospace; line-height: 1; }
.oa-big-sub { font-size: 12px; color: var(--muted); margin-top: 6px; }
.oa-breakdown { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.oa-chip { background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; text-align: center; }
.oa-chip-lbl { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .6px; color: var(--muted); margin-bottom: 3px; }
.oa-chip-val { font-size: 20px; font-weight: 800; font-family: 'IBM Plex Mono', monospace; }
.oa-chip-val.processed { color: var(--g1); }
.oa-chip-val.pending { color: var(--amber); }
.oa-chip-val.rejected { color: var(--red); }
.oa-chip-val.docs { color: var(--blue); }
.oa-chip-val.pages { color: var(--teal); }
.oa-right { display: flex; flex-direction: column; gap: 18px; }
/* ============================================================
END BLOCK: OTHER ACTIONS TAB STYLES
============================================================ */
/* ============================================================
BLOCK: MANUAL ENTRY TAB STYLES
============================================================ */
.entry-intro {
display: flex; align-items: flex-start; gap: 14px;
background: rgba(217,91,10,.06); border: 1px solid rgba(217,91,10,.25);
border-radius: 11px; padding: 16px 20px; margin-bottom: 22px;
}
.entry-intro-icon { font-size: 22px; flex-shrink: 0; margin-top: 2px; }
.entry-intro-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--orange); margin-bottom: 4px; }
.entry-intro-text { font-size: 13px; color: var(--body); line-height: 1.6; }
.entry-intro-text strong { color: var(--ink); font-weight: 600; }
/* Employee selector row */
.entry-select-row {
display: flex; align-items: center; gap: 14px;
background: var(--surface); border: 1px solid var(--border);
border-radius: 10px; padding: 14px 18px; margin-bottom: 20px;
flex-wrap: wrap;
}
.entry-select-row label { font-size: 13px; font-weight: 600; color: var(--ink); flex-shrink: 0; }
.entry-emp-select {
padding: 8px 13px; border: 1.5px solid var(--border);
border-radius: 7px; background: #fff; color: var(--ink);
font-size: 13px; font-weight: 500; outline: none; cursor: pointer;
min-width: 220px; flex-shrink: 0;
}
.entry-emp-select:focus { border-color: var(--orange); }
.entry-save-btn {
padding: 8px 22px; background: var(--g1); color: #fff;
border: none; border-radius: 7px; font-size: 13px; font-weight: 600;
cursor: pointer; transition: .18s; margin-left: auto;
}
.entry-save-btn:hover { background: var(--g2); transform: translateY(-1px); }
.entry-save-btn.saved { background: var(--g3); }
/* Score entry grid */
.entry-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px,1fr)); gap: 12px; margin-bottom: 20px; }
.entry-field { display: flex; flex-direction: column; gap: 5px; }
.entry-field label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .7px; color: var(--muted); }
.entry-field input[type=number] {
padding: 10px 13px; border: 1.5px solid var(--border);
border-radius: 8px; background: #fff; color: var(--ink);
font-size: 16px; font-weight: 600; outline: none; transition: .2s;
font-family: 'IBM Plex Mono', monospace;
}
.entry-field input[type=number]:focus { border-color: var(--orange); box-shadow: 0 0 0 3px rgba(217,91,10,.1); }
.entry-field input[type=number]:valid { border-color: var(--g3); }
.entry-hint { font-size: 10px; color: var(--muted); }
/* Specialist entry */
.spec-entry-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px,1fr)); gap: 12px; margin-bottom: 18px; }
.spec-entry-field { display: flex; flex-direction: column; gap: 5px; }
.spec-entry-field label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .7px; color: var(--muted); }
.spec-entry-field input[type=number] {
padding: 10px 13px; border: 1.5px solid var(--spec3);
border-radius: 8px; background: var(--spec2); color: var(--ink);
font-size: 16px; font-weight: 600; outline: none; transition: .2s;
font-family: 'IBM Plex Mono', monospace;
}
.spec-entry-field input[type=number]:focus { border-color: var(--spec); box-shadow: 0 0 0 3px rgba(124,58,237,.1); }
.spec-save-btn {
padding: 8px 22px; background: var(--spec); color: #fff;
border: none; border-radius: 7px; font-size: 13px; font-weight: 600;
cursor: pointer; transition: .18s;
}
.spec-save-btn:hover { background: #6030C0; transform: translateY(-1px); }
.spec-save-btn.saved { background: var(--g3); color: #fff; }
/* Feedback toast */
.toast {
position: fixed; bottom: 28px; right: 28px; z-index: 8888;
background: var(--g1); color: #fff;
padding: 12px 22px; border-radius: 9px;
font-size: 13px; font-weight: 600;
box-shadow: 0 4px 16px rgba(10,92,56,.3);
transform: translateY(80px); opacity: 0;
transition: transform .3s ease, opacity .3s ease;
}
.toast.show { transform: translateY(0); opacity: 1; }
.toast.spec-toast { background: var(--spec); box-shadow: 0 4px 16px rgba(124,58,237,.3); }
/* ============================================================
END BLOCK: MANUAL ENTRY TAB STYLES
============================================================ */
/* ============================================================
BLOCK: LAYOUT HELPERS
============================================================ */
.ins-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 18px; }
.ins-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(400px,1fr)); gap: 14px; }
/* ============================================================
END BLOCK: LAYOUT HELPERS
============================================================ */
</style>
</head>
<body>
<!-- ============================================================
BLOCK: EXPORT OVERLAY HTML
============================================================ -->
<div class="exp-overlay" id="expOverlay">
<div class="exp-spin"></div>
<div class="exp-msg" id="expMsg">Generating…</div>
<div class="exp-sub" id="expSub">Please wait</div>
</div>
<!-- ============================================================
END BLOCK: EXPORT OVERLAY HTML
============================================================ -->
<!-- ============================================================
BLOCK: SAVE FEEDBACK TOAST
============================================================ -->
<div class="toast" id="toast">✓ Scores saved — charts updated</div>
<!-- ============================================================
END BLOCK: SAVE FEEDBACK TOAST
============================================================ -->
<!-- ============================================================
BLOCK: HEADER HTML
============================================================ -->
<header>
<div class="logo">
<div class="logo-icon">HR</div>
<div>
<div class="logo-main">Performance Analytics</div>
<div class="logo-sub">Human Resources Dashboard</div>
</div>
</div>
<div class="hdr-right">
<button class="exp-btn pdf" onclick="exportPDF()">📄 Export PDF</button>
<button class="exp-btn pptx" onclick="exportPPTX()">📊 Export PPTX</button>
<button class="exp-btn all" onclick="exportBoth()">⬇ Export All</button>
</div>
</header>
<!-- ============================================================
END BLOCK: HEADER HTML
============================================================ -->
<!-- ============================================================
BLOCK: STATUS BAR HTML
============================================================ -->
<div class="status-bar">
<div class="s-pill"><span class="s-dot"></span>6 Ranked Employees</div>
<div class="s-pill"><span class="s-dot spec"></span>1 Specialist Employee</div>
<div class="s-pill"><span class="s-dot"></span>14 Services</div>
<div class="s-pill">
<span class="s-dot" style="background:var(--g1);box-shadow:0 0 0 2px var(--g6)"></span>
Grand Total: <strong id="gtVal" style="color:var(--g1);font-family:'IBM Plex Mono',monospace;font-size:11px;margin-left:4px;font-weight:700">—</strong>
</div>
<div class="s-pill">
<span class="s-dot spec"></span>
IT Processed: <strong id="itProcStatus" style="color:var(--spec);font-family:'IBM Plex Mono',monospace;font-size:11px;margin-left:4px;font-weight:700">—</strong>
</div>
</div>
<!-- ============================================================
END BLOCK: STATUS BAR HTML
============================================================ -->
<!-- ============================================================
BLOCK: TAB BAR HTML
============================================================ -->
<div class="tab-bar">
<button class="tab-btn active" data-tab="tp-insights">Insights</button>
<button class="tab-btn" 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" data-tab="tp-other">Other Actions</button>
<button class="tab-btn" data-tab="tp-entry">📝 Data Entry</button>
</div>
<!-- ============================================================
END BLOCK: TAB BAR HTML
============================================================ -->
<!-- ============================================================
BLOCK: TAB PANE - INSIGHTS
============================================================ -->
<div class="tab-pane active" id="tp-insights">
<div class="sec-t">Performance Intelligence — 6 Ranked Employees</div>
<div class="ins-kpi" id="insKpi"></div>
<div class="ins-2col">
<div class="lb-wrap">
<div class="lb-title">🏆 Overall Ranking — 6 Employees</div>
<div id="lbList"></div>
</div>
<div class="chart-card">
<div class="cc-title"><span>📡</span>Team Average by Service</div>
<div class="cc-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 Analysis</div>
<div class="ins-grid" id="insGrid"></div>
</div>
<!-- ============================================================
END BLOCK: TAB PANE - INSIGHTS
============================================================ -->
<!-- ============================================================
BLOCK: TAB PANE - PER EMPLOYEE
============================================================ -->
<div class="tab-pane" id="tp-emp">
<div class="sec-t">KPI Overview — 6 Ranked Employees</div>
<div class="kpi-strip" id="empKpi"></div>
<div class="sec-t">Employee Performance Cards</div>
<div class="emp-grid" id="empGrid"></div>
</div>
<!-- ============================================================
END BLOCK: TAB PANE - PER EMPLOYEE
============================================================ -->
<!-- ============================================================
BLOCK: TAB PANE - PER SERVICE
============================================================ -->
<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 — 6 Employees</div>
<div class="svc-grid" id="svcGrid"></div>
</div>
<!-- ============================================================
END BLOCK: TAB PANE - PER SERVICE
============================================================ -->
<!-- ============================================================
BLOCK: TAB PANE - DISTRIBUTION
============================================================ -->
<div class="tab-pane" id="tp-dist">
<div class="sec-t">Score Distribution by Service</div>
<div class="dist-grid" id="distGrid"></div>
</div>
<!-- ============================================================
END BLOCK: TAB PANE - DISTRIBUTION
============================================================ -->
<!-- ============================================================
BLOCK: TAB PANE - OTHER ACTIONS (Specialist employee stats)
============================================================ -->
<div class="tab-pane" id="tp-other">
<div class="sec-t">Specialist Employee — Internal Transfer Focus</div>
<div class="oa-layout">
<!-- Left: stats card -->
<div class="oa-total-card">
<div class="oa-emp-badge">
<div class="oa-emp-av" id="oaSpecAv">?</div>
<div>
<div class="oa-emp-name" id="oaSpecName">Specialist Employee</div>
<div class="oa-emp-sub" id="oaSpecId">—</div>
</div>
</div>
<div class="oa-big-stat">
<div class="oa-big-lbl">Total IT Requests Processed</div>
<div class="oa-big-val" id="oaItTotal">0</div>
<div class="oa-big-sub" id="oaItSub">Enter data via Data Entry tab</div>
</div>
<div class="oa-breakdown">
<div class="oa-chip"><div class="oa-chip-lbl">Processed</div><div class="oa-chip-val processed" id="oaProc">0</div></div>
<div class="oa-chip"><div class="oa-chip-lbl">Pending</div> <div class="oa-chip-val pending" id="oaPend">0</div></div>
<div class="oa-chip"><div class="oa-chip-lbl">Rejected</div> <div class="oa-chip-val rejected" id="oaRej">0</div></div>
<div class="oa-chip"><div class="oa-chip-lbl">Docs Scanned</div><div class="oa-chip-val docs" id="oaDocs">0</div></div>
<div class="oa-chip" style="grid-column:1/-1;">
<div class="oa-chip-lbl">Total Pages Scanned</div>
<div class="oa-chip-val pages" id="oaPages">0</div>
</div>
</div>
</div>
<!-- Right: chart -->
<div class="oa-right">
<div class="chart-card" style="border-color:rgba(14,138,138,.3);">
<div class="cc-title" style="color:var(--teal);"><span>🗂️</span>Documents Scanned per HR Service</div>
<div style="position:relative;height:280px;"><canvas id="docScanChart"></canvas></div>
</div>
</div>
</div>
</div>
<!-- ============================================================
END BLOCK: TAB PANE - OTHER ACTIONS
============================================================ -->
<!-- ============================================================
BLOCK: TAB PANE - DATA ENTRY
This tab handles ALL manual data entry for:
1. 6 ranked employees (service scores 0-100)
2. 1 specialist employee (IT requests + doc scans)
============================================================ -->
<div class="tab-pane" id="tp-entry">
<!-- Intro banner -->
<div class="entry-intro">
<div class="entry-intro-icon">📝</div>
<div>
<div class="entry-intro-title">Manual Data Entry</div>
<div class="entry-intro-text">
Select an employee from the dropdown, enter their scores for each service (0–100),
then click <strong>Save & Update Charts</strong>. All charts and rankings update instantly.
Use the Specialist section at the bottom to enter Internal Transfer and document scan data.
</div>
</div>
</div>
<!-- ── RANKED EMPLOYEE SECTION ── -->
<div class="sec-t">Ranked Employee Scores (0 – 100 per service)</div>
<!-- Employee selector + save button -->
<div class="entry-select-row">
<label for="entryEmpSelect">Employee:</label>
<select id="entryEmpSelect" class="entry-emp-select"></select>
<button class="entry-save-btn" id="entrySaveBtn" onclick="saveEmployeeScores()">
✓ Save & Update Charts
</button>
</div>
<!-- Score input grid — dynamically built by JS -->
<div class="entry-grid" id="entryGrid"></div>
<!-- ── SPECIALIST EMPLOYEE SECTION ── -->
<div class="sec-t" style="margin-top:12px;">Specialist Employee — <span style="color:var(--spec);">Internal Transfer & Document Data</span></div>
<div style="background:var(--spec2);border:1.5px solid var(--spec3);border-radius:12px;padding:20px;margin-bottom:20px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;padding-bottom:14px;border-bottom:1px solid var(--spec3);">
<div style="width:38px;height:38px;border-radius:9px;background:linear-gradient(135deg,var(--spec),var(--blue));display:flex;align-items:center;justify-content:center;font-family:'IBM Plex Mono',monospace;font-size:13px;font-weight:700;color:#fff;flex-shrink:0;" id="specAvEntry">?</div>
<div>
<div style="font-size:14px;font-weight:700;color:var(--ink);" id="specNameEntry">Specialist Employee</div>
<div style="font-size:10px;color:var(--muted);" id="specIdEntry">—</div>
</div>
</div>
<!-- IT Request counts -->
<div style="margin-bottom:16px;">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--muted);margin-bottom:10px;">Internal Transfer Requests</div>
<div class="spec-entry-grid">
<div class="spec-entry-field">
<label>Requests Processed</label>
<input type="number" id="spec_processed" min="0" value="0" placeholder="0">
</div>
<div class="spec-entry-field">
<label>Requests Pending</label>
<input type="number" id="spec_pending" min="0" value="0" placeholder="0">
</div>
<div class="spec-entry-field">
<label>Requests Rejected</label>
<input type="number" id="spec_rejected" min="0" value="0" placeholder="0">
</div>
</div>
</div>
<!-- Doc scan counts per service — dynamically built -->
<div style="margin-bottom:16px;">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--muted);margin-bottom:10px;">Documents Scanned per Service</div>
<div class="spec-entry-grid" id="specDocGrid"></div>
</div>
<button class="spec-save-btn" id="specSaveBtn" onclick="saveSpecialistData()">
✓ Save Specialist Data & Update Charts
</button>
</div>
</div>
<!-- ============================================================
END BLOCK: TAB PANE - DATA ENTRY
============================================================ -->
<!-- ============================================================
BLOCK: JAVASCRIPT — DATA CONFIG
Edit RANKED_EMPLOYEES, SERVICES, and SPECIALIST here
============================================================ -->
<script>
/* ============================================================
BLOCK: EMPLOYEES CONFIG
──────────────────────────────────────────────────────────
STEP-BY-STEP: How to edit employee names in Notepad
──────────────────────────────────────────────────────────
STEP 1 — Open this file in Notepad (or Notepad++)
STEP 2 — Press Ctrl+F and search for: BLOCK: EMPLOYEES CONFIG
STEP 3 — Find the list below (id and name pairs)
STEP 4 — Change the name value inside the quotes
Example: change 'Osama' to 'Osama Al-Ghamdi'
STEP 5 — Keep exactly 6 entries (do NOT add or remove rows)
STEP 6 — The id (e.g. 'EMP001') must match the id used in
BLOCK: INITIAL SCORES below — keep them in sync
STEP 7 — Save the file, then refresh in your browser
──────────────────────────────────────────────────────────
Keep exactly 6 entries for the ranking tabs.
============================================================ */
const RANKED_EMPLOYEES = [
{ id: 'EMP001', name: 'Osama' },
{ id: 'EMP002', name: 'Alhamdan' },
{ id: 'EMP003', name: 'Alzahri' },
{ id: 'EMP004', name: 'Omar' },
{ id: 'EMP005', name: 'Alaslani' },
{ id: 'EMP006', name: 'Marwa' }
];
/* ============================================================
END BLOCK: EMPLOYEES CONFIG
============================================================ */
/* ============================================================
BLOCK: SPECIALIST CONFIG
──────────────────────────────────────────────────────────
STEP-BY-STEP: How to edit the Specialist name/role
──────────────────────────────────────────────────────────
STEP 1 — Press Ctrl+F and search for: BLOCK: SPECIALIST CONFIG
STEP 2 — Find the name and role lines below
STEP 3 — Change the text inside the quotes
Example: change 'Runel' to 'Runel Santos'
STEP 4 — Save the file and refresh in your browser
STEP 5 — To change IT requests and document scan numbers,
see BLOCK: SPECIALIST INITIAL DATA below
──────────────────────────────────────────────────────────
The Specialist is the 7th employee focused on Internal
Transfer work. They are NOT included in the ranking.
============================================================ */
const SPECIALIST = {
id: 'EMP007',
name: 'Runel',
role: 'Internal Transfer Specialist'
};
/* ============================================================
END BLOCK: SPECIALIST CONFIG
============================================================ */
/* ============================================================
BLOCK: SERVICES CONFIG
──────────────────────────────────────────────────────────
STEP-BY-STEP: How to edit service display names
──────────────────────────────────────────────────────────
STEP 1 — Press Ctrl+F and search for: BLOCK: SERVICES CONFIG
STEP 2 — Each service has two parts:
key — the internal code (DO NOT change this)
label — the display name shown in charts/tables
STEP 3 — Only change the label text inside the quotes
Example: change 'Hiring' to 'Recruitment & Hiring'
STEP 4 — Do NOT change the key — it links to score data
STEP 5 — Save the file and refresh in your browser
──────────────────────────────────────────────────────────
Keep exactly 14 entries to match the INITIAL SCORES block.
============================================================ */
const SERVICES = [
{ key: 'hiring', label: 'Hiring' },
{ key: 'wages', label: 'Wages' },
{ key: 'salary_advance', label: 'Salary Advance' },
{ key: 'education_program', label: 'Education Program' },
{ key: 'work_schedule_ot', label: 'Work Schedule & OT' },
{ key: 'employee_status_update',label: 'Status Update' },
{ key: 'timekeeping_system', label: 'Timekeeping' },
{ key: 'internal_transfer', label: 'Internal Transfer' },
{ key: 'medical_insurance', label: 'Medical Insurance' },
{ key: 'hop_distribution', label: 'HOP Distribution' },
{ key: 'muskan_distribution', label: 'Muskan Distribution' },
{ key: 'home_loan_program', label: 'Home Loan Program' },
{ key: 'housing_issues', label: 'Housing Issues' },
{ key: 'separation', label: 'Separation' }
];
/* ============================================================
END BLOCK: SERVICES CONFIG
============================================================ */
/* ============================================================
BLOCK: INITIAL SCORES
──────────────────────────────────────────────────────────
STEP-BY-STEP: How to edit employee scores in Notepad
──────────────────────────────────────────────────────────
STEP 1 — Press Ctrl+F and search for: BLOCK: INITIAL SCORES
STEP 2 — Find the employee you want to update by their ID
e.g. EMP001 is Osama, EMP002 is Alhamdan, etc.
STEP 3 — Find the service you want to change inside that
employee's block, e.g. hiring: 20
STEP 4 — Change the number after the colon
Example: change hiring: 20 to hiring: 45
STEP 5 — Do NOT change the service key (the word before :)
Only change the number after :
STEP 6 — Numbers can be any positive whole number (0 or more)
These are transaction counts, NOT percentages
STEP 7 — Save the file and refresh in your browser
Charts and rankings will update automatically
──────────────────────────────────────────────────────────
Each employee block must have ALL 14 service keys.
Use 0 for services that have no transactions.
============================================================ */
let DATA = {
/* ── EMP001: Osama ── */
EMP001: {
hiring: 20,
wages: 30,
salary_advance: 40,
education_program: 50,
work_schedule_ot: 60,
employee_status_update: 50,
timekeeping_system: 30,
internal_transfer: 300,
medical_insurance: 0,
hop_distribution: 0,
muskan_distribution: 0,
home_loan_program: 0,
housing_issues: 0,
separation: 20
},
/* ── EMP002: Alhamdan ── */
EMP002: {
hiring: 20,
wages: 30,
salary_advance: 40,
education_program: 50,
work_schedule_ot: 60,
employee_status_update: 50,
timekeeping_system: 30,
internal_transfer: 220,
medical_insurance: 0,
hop_distribution: 0,
muskan_distribution: 0,
home_loan_program: 0,
housing_issues: 0,
separation: 20
},
/* ── EMP003: Alzahri ── */
EMP003: {
hiring: 20,
wages: 30,
salary_advance: 40,
education_program: 50,
work_schedule_ot: 60,
employee_status_update: 50,
timekeeping_system: 30,
internal_transfer: 220,
medical_insurance: 0,
hop_distribution: 0,
muskan_distribution: 0,
home_loan_program: 0,
housing_issues: 0,
separation: 20
},
/* ── EMP004: Omar ── */
EMP004: {
hiring: 20,
wages: 30,
salary_advance: 40,
education_program: 50,
work_schedule_ot: 60,
employee_status_update: 50,
timekeeping_system: 30,
internal_transfer: 220,
medical_insurance: 0,
hop_distribution: 0,
muskan_distribution: 0,
home_loan_program: 0,
housing_issues: 0,
separation: 20
},
/* ── EMP005: Alaslani ── */
EMP005: {
hiring: 20,
wages: 30,
salary_advance: 40,
education_program: 50,
work_schedule_ot: 60,
employee_status_update: 50,
timekeeping_system: 30,
internal_transfer: 220,
medical_insurance: 0,
hop_distribution: 0,
muskan_distribution: 0,
home_loan_program: 0,
housing_issues: 0,
separation: 20
},
/* ── EMP006: Marwa ── */
EMP006: {
hiring: 20,
wages: 30,
salary_advance: 40,
education_program: 50,
work_schedule_ot: 60,
employee_status_update: 50,
timekeeping_system: 30,
internal_transfer: 220,
medical_insurance: 0,
hop_distribution: 0,
muskan_distribution: 0,
home_loan_program: 0,
housing_issues: 0,
separation: 20
}
};
/* ============================================================
END BLOCK: INITIAL SCORES
============================================================ */
/* ============================================================
BLOCK: SPECIALIST INITIAL DATA
──────────────────────────────────────────────────────────
STEP-BY-STEP: How to edit Specialist data in Notepad
──────────────────────────────────────────────────────────
STEP 1 — Press Ctrl+F and search for: BLOCK: SPECIALIST INITIAL DATA
STEP 2 — To change IT request counts, find and edit:
processed: 1230 ← total requests processed
pending: 0 ← requests still in progress
rejected: 0 ← requests that were rejected
STEP 3 — To change documents scanned per service, find the
doc_scans section below and edit the docs number
for each service. Example:
hiring: { docs: 15 } ← change 15 to new value
STEP 4 — Services NOT listed below default to 0 documents
If a service should show 0, set docs: 0
STEP 5 — Save the file and refresh in your browser
The Other Actions tab and charts update instantly
──────────────────────────────────────────────────────────
doc_scans keys must match the keys in SERVICES CONFIG above
============================================================ */
let SPEC_DATA = {
processed: 1230, /* ← EDIT: Total IT requests processed by Runel */
pending: 0, /* ← EDIT: IT requests still pending */
rejected: 0, /* ← EDIT: IT requests rejected */
/* Documents scanned by service — edit the docs number for each */
doc_scans: {
hiring: { docs: 15, pages: 0 }, /* ← Hiring = 15 */
wages: { docs: 123, pages: 0 }, /* ← Wages = 123 */
salary_advance: { docs: 0, pages: 0 }, /* ← Salary Advance = 0 */
education_program: { docs: 133, pages: 0 }, /* ← Education Program = 133 */
work_schedule_ot: { docs: 0, pages: 0 }, /* ← Work Schedule & OT = 0 */
employee_status_update: { docs: 0, pages: 0 }, /* ← Status Update = 0 */
timekeeping_system: { docs: 0, pages: 0 }, /* ← Timekeeping = 0 */
internal_transfer: { docs: 1350, pages: 0 }, /* ← Internal Transfer = 1350 */
medical_insurance: { docs: 0, pages: 0 }, /* ← Medical Insurance = 0 */
hop_distribution: { docs: 300, pages: 0 }, /* ← HOP Distribution = 300 */
muskan_distribution: { docs: 0, pages: 0 }, /* ← Muskan Distribution = 0 */
home_loan_program: { docs: 0, pages: 0 }, /* ← Home Loan Program = 0 */
housing_issues: { docs: 0, pages: 0 }, /* ← Housing Issues = 0 */
separation: { docs: 30, pages: 0 } /* ← Separation = 30 */
}
};
/* ============================================================
END BLOCK: SPECIALIST INITIAL DATA
============================================================ */
/* ============================================================
BLOCK: CHART COLOUR PALETTE
One colour per employee (6 entries).
Change hex values to retheme the charts.
Must match number of RANKED_EMPLOYEES.
============================================================ */
const ECOLS = [
'#1A5CB4', // EMP001
'#138050', // EMP002
'#6836D0', // EMP003
'#D95B0A', // EMP004
'#B57D00', // EMP005
'#0E8A8A' // EMP006
];
/* ============================================================
END BLOCK: CHART COLOUR PALETTE
============================================================ */
/* ============================================================
BLOCK: AVATAR GRADIENTS
Background gradients for employee avatar circles.
Must match number of RANKED_EMPLOYEES.
============================================================ */
const AVT = [
'linear-gradient(135deg,#1A5CB4,#0E8A8A)',
'linear-gradient(135deg,#138050,#0A5C38)',
'linear-gradient(135deg,#6836D0,#1A5CB4)',
'linear-gradient(135deg,#D95B0A,#B57D00)',
'linear-gradient(135deg,#B57D00,#D95B0A)',
'linear-gradient(135deg,#0E8A8A,#138050)'
];
/* ============================================================
END BLOCK: AVATAR GRADIENTS
============================================================ */
/* ============================================================
BLOCK: CHART INSTANCES (internal — do not edit)
============================================================ */
const CH = { emp:{}, svc:{}, dist:{}, ins:{}, oa:{} };
/* ============================================================
END BLOCK: CHART INSTANCES
============================================================ */
/* ============================================================
BLOCK: DATA HELPER FUNCTIONS
============================================================ */
function EA() { return RANKED_EMPLOYEES.map(e => ({ ...DATA[e.id], id: e.id, name: e.name })); }
function eTotal(e) { return SERVICES.reduce((s, sv) => s + (e[sv.key] || 0), 0); }
function eAvg(e) { return eTotal(e) / SERVICES.length; }
function eBest(e) { return SERVICES.reduce((b, s) => (e[s.key]||0) > (e[b.key]||0) ? s : b, SERVICES[0]); }
function eWorst(e) { return SERVICES.reduce((w, s) => (e[s.key]||0) < (e[w.key]||0) ? s : w, SERVICES[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(); }
/* ============================================================
END BLOCK: DATA HELPER FUNCTIONS
============================================================ */
/* ============================================================
BLOCK: CHART.JS DEFAULTS
============================================================ */
Chart.defaults.color = '#68788E';
Chart.defaults.borderColor = '#DDE3EC';
Chart.defaults.font.family = 'Inter';
Chart.defaults.font.size = 12;
const CB = () => ({
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: '#0D1825',
borderColor: '#1A5CB4', borderWidth: 1,
titleColor: '#fff', bodyColor: '#A0B0C0',
padding: 10, cornerRadius: 7,
titleFont: { size: 13, weight: '700' },
bodyFont: { size: 12 }
}
}
});
function destroyCharts(obj) {
Object.values(obj).forEach(c => { try { c && c.destroy && c.destroy(); } catch(e) {} });
}
/* ============================================================
END BLOCK: CHART.JS DEFAULTS
============================================================ */
/* ============================================================
BLOCK: RENDER — OTHER ACTIONS TAB (Specialist stats)
============================================================ */
function renderOA() {
// Update specialist header
document.getElementById('oaSpecAv').textContent = INI(SPECIALIST.name);
document.getElementById('oaSpecName').textContent = SPECIALIST.name;
document.getElementById('oaSpecId').textContent = SPECIALIST.id + ' · ' + SPECIALIST.role;
// IT request totals
const proc = SPEC_DATA.processed || 0;
const pend = SPEC_DATA.pending || 0;
const rej = SPEC_DATA.rejected || 0;
document.getElementById('oaItTotal').textContent = proc;
document.getElementById('oaItSub').textContent = (proc + pend + rej) + ' total requests logged';
document.getElementById('oaProc').textContent = proc;
document.getElementById('oaPend').textContent = pend;
document.getElementById('oaRej').textContent = rej;
document.getElementById('itProcStatus').textContent = proc;
// Doc scan totals
let totalDocs = 0, totalPages = 0;
Object.values(SPEC_DATA.doc_scans).forEach(d => {
totalDocs += (parseInt(d.docs) || 0);
totalPages += (parseInt(d.pages) || 0);
});
document.getElementById('oaDocs').textContent = totalDocs;
document.getElementById('oaPages').textContent = totalPages.toLocaleString();
// Doc scan bar chart
if (CH.oa.docScan) CH.oa.docScan.destroy();
const dsLabels = SERVICES.map(s => s.label);
const dsVals = SERVICES.map(s => parseInt((SPEC_DATA.doc_scans[s.key] || {}).docs) || 0);
CH.oa.docScan = new Chart(document.getElementById('docScanChart').getContext('2d'), {
type: 'bar',
data: {
labels: dsLabels,
datasets: [{
label: 'Documents Scanned',
data: dsVals,
backgroundColor: dsVals.map((_, i) => ECOLS[i % ECOLS.length] + '33'),
borderColor: dsVals.map((_, i) => ECOLS[i % ECOLS.length]),
borderWidth: 2, borderRadius: 6, borderSkipped: false
}]
},
options: {
...CB(),
plugins: { ...CB().plugins, legend: { display: false } },
scales: {
y: { beginAtZero: true, grid: { color: '#EDF0F4' }, ticks: { font: { size: 11 } } },
x: { grid: { display: false }, ticks: { font: { size: 10 }, color: '#2C3A4E', maxRotation: 35 } }
}
}
});
}
/* ============================================================
END BLOCK: RENDER — OTHER ACTIONS TAB
============================================================ */
/* ============================================================
BLOCK: RENDER — INSIGHTS TAB
============================================================ */
function renderInsights() {
const arr = EA();
const sorted = [...arr].sort((a, b) => eTotal(b) - eTotal(a));
const gt = GT();
const pct = ((gt / (arr.length * SERVICES.length * 100)) * 100).toFixed(1);
const top = sorted[0], bot = sorted[sorted.length - 1];
const bSvc = SERVICES.reduce((b, s) => sTotal(s.key) > sTotal(b.key) ? s : b, SERVICES[0]);
const wSvc = SERVICES.reduce((b, s) => sTotal(s.key) < sTotal(b.key) ? s : b, SERVICES[0]);
// KPI row
document.getElementById('insKpi').innerHTML = `
<div class="ik green"><div class="ik-ico">🎯</div><div><div class="ik-lbl">Efficiency</div><div class="ik-val">${pct}%</div><div class="ik-sub">of max possible</div></div></div>
<div class="ik blue"><div class="ik-ico">📊</div><div><div class="ik-lbl">Grand Total</div><div class="ik-val">${gt}</div><div class="ik-sub">${arr.length} emp × ${SERVICES.length} svc</div></div></div>
<div class="ik orange"><div class="ik-ico">🏆</div><div><div class="ik-lbl">Top Performer</div><div class="ik-val">${top.name.split(' ')[0]}</div><div class="ik-sub">Score: ${eTotal(top)}</div></div></div>
<div class="ik red"><div class="ik-ico">📌</div><div><div class="ik-lbl">Needs Support</div><div class="ik-val">${bot.name.split(' ')[0]}</div><div class="ik-sub">Gap: ${eTotal(top) - eTotal(bot)}</div></div></div>
<div class="ik teal"><div class="ik-ico">⬆</div><div><div class="ik-lbl">Best Service</div><div class="ik-val">${bSvc.label}</div><div class="ik-sub">Total: ${sTotal(bSvc.key)}</div></div></div>
<div class="ik purple"><div class="ik-ico">⬇</div><div><div class="ik-lbl">Weakest Svc</div><div class="ik-val">${wSvc.label}</div><div class="ik-sub">Total: ${sTotal(wSvc.key)}</div></div></div>
`;
document.getElementById('gtVal').textContent = gt;
// Leaderboard
const lb = document.getElementById('lbList'); lb.innerHTML = '';
const maxT = eTotal(sorted[0]) || 1;
sorted.forEach((emp, i) => {
const t = eTotal(emp);
const ei = arr.indexOf(arr.find(a => a.id === emp.id));
const col = ECOLS[ei % ECOLS.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]}">${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 by service chart
if (CH.ins.ta) CH.ins.ta.destroy();
CH.ins.ta = new Chart(document.getElementById('teamAvgChart').getContext('2d'), {
type: 'bar',
data: {
labels: SERVICES.map(s => s.label),
datasets: [{
data: SERVICES.map(s => sAvg(s.key)),
backgroundColor: SERVICES.map((_, i) => ECOLS[i % ECOLS.length] + '33'),
borderColor: SERVICES.map((_, i) => ECOLS[i % ECOLS.length]),
borderWidth: 2, borderRadius: 4, borderSkipped: false
}]
},
options: { ...CB(), indexAxis: 'y', scales: {
x: { beginAtZero: true, max: 100, grid: { color: '#EDF0F4' }, ticks: { font: { size: 10 } } },
y: { grid: { display: false }, ticks: { font: { size: 10 }, color: '#2C3A4E' } }
}}
});
// Individual radar charts
destroyCharts(CH.ins.radars || {}); CH.ins.radars = {};
const rg = document.getElementById('radarGrid'); rg.innerHTML = '';
arr.forEach((emp, i) => {
const col = ECOLS[i % ECOLS.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]}">${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: SERVICES.map(s => s.label),
datasets: [{ data: SERVICES.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: '#DDE3EC' }, angleLines: { color: '#DDE3EC' },
pointLabels: { color: '#68788E', font: { size: 8 } },
ticks: { backdropColor: 'transparent', color: '#9AAAB9', font: { size: 8 }, stepSize: 25 }
}}}
});
});
// Comparative insight charts
destroyCharts(CH.ins.extra || {}); CH.ins.extra = {};
const ig = document.getElementById('insGrid'); ig.innerHTML = '';
const mkC = (id, title, icon, col) => {
const c = document.createElement('div'); c.className = 'chart-card';
c.innerHTML = `<div class="cc-title" style="color:${col}"><span>${icon}</span>${title}</div><div class="cc-ch"><canvas id="${id}"></canvas></div>`;
ig.appendChild(c); return c;
};
// Total score bar
const srtd = [...arr].sort((a, b) => eTotal(b) - eTotal(a));
const cA = mkC('ins-tot', 'Total Score per Employee', '📈', 'var(--blue)');
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[arr.findIndex(a => a.id === e.id) % ECOLS.length] + '44'),
borderColor: srtd.map(e => ECOLS[arr.findIndex(a => a.id === e.id) % ECOLS.length]),
borderWidth: 2, borderRadius: 5, borderSkipped: false }]
},
options: { ...CB(), scales: {
y: { beginAtZero: true, grid: { color: '#EDF0F4' }, ticks: { font: { size: 11 } } },
x: { grid: { display: false }, ticks: { font: { size: 12 }, color: '#2C3A4E' } }
}}
});
// Performance gap bar
const gaps = SERVICES.map(s => { const v = arr.map(e => e[s.key] || 0); return Math.max(...v) - Math.min(...v); });
const cC = mkC('ins-gap', 'Performance Gap per Service (Max − Min)', '⚡', 'var(--orange)');
CH.ins.extra.gap = new Chart(cC.querySelector('#ins-gap').getContext('2d'), {
type: 'bar',
data: { labels: SERVICES.map(s => s.label),
datasets: [{ data: gaps,
backgroundColor: gaps.map(g => g > 40 ? '#C4303033' : g > 20 ? '#B57D0033' : '#13805033'),
borderColor: gaps.map(g => g > 40 ? '#C43030' : g > 20 ? '#B57D00' : '#138050'),
borderWidth: 2, borderRadius: 4, borderSkipped: false }]
},
options: { ...CB(), indexAxis: 'y', scales: {
x: { beginAtZero: true, grid: { color: '#EDF0F4' }, ticks: { font: { size: 10 } } },
y: { grid: { display: false }, ticks: { font: { size: 10 }, color: '#2C3A4E' } }
}}
});
// Average score line
const cD = mkC('ins-avg', 'Average Score per Employee', '📉', 'var(--g1)');
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: '#138050', backgroundColor: '#13805018',
pointBackgroundColor: ECOLS, pointRadius: 7, pointHoverRadius: 9,
borderWidth: 2.5, tension: .35, fill: true }]
},
options: { ...CB(), scales: {
y: { beginAtZero: true, max: 100, grid: { color: '#EDF0F4' }, ticks: { font: { size: 11 } } },
x: { grid: { display: false }, ticks: { font: { size: 12 }, color: '#2C3A4E' } }
}}
});
// Service volume polar
const cB = mkC('ins-pol', 'Service Volume (Polar)', '🌐', 'var(--purple)');
CH.ins.extra.pol = new Chart(cB.querySelector('#ins-pol').getContext('2d'), {
type: 'polarArea',
data: { labels: SERVICES.map(s => s.label),
datasets: [{ data: SERVICES.map(s => sTotal(s.key)),
backgroundColor: ECOLS.map(c => c + '55'), borderColor: ECOLS, borderWidth: 1.5 }]
},
options: { ...CB(), scales: { r: { beginAtZero: true, grid: { color: '#DDE3EC' }, ticks: { backdropColor: 'transparent', color: '#9AAAB9', font: { size: 8 } } } },
plugins: { ...CB().plugins, legend: { display: true, position: 'right', labels: { color: '#2C3A4E', font: { size: 11 }, boxWidth: 11, padding: 7 } } }
}
});
}
/* ============================================================
END BLOCK: RENDER — INSIGHTS TAB
============================================================ */
/* ============================================================
BLOCK: RENDER — PER EMPLOYEE TAB
============================================================ */
function renderEmpKpi() {
const arr = EA(), gt = GT(), avg = (gt / (arr.length * SERVICES.length)).toFixed(1);
const srt = [...arr].sort((a, b) => eTotal(b) - eTotal(a));
const bS = SERVICES.reduce((b, s) => sTotal(s.key) > sTotal(b.key) ? s : b, SERVICES[0]);
const wS = SERVICES.reduce((b, s) => sTotal(s.key) < sTotal(b.key) ? s : b, SERVICES[0]);
document.getElementById('empKpi').innerHTML = `
<div class="kpi-card cg"><div class="kpi-lbl">Grand Total</div><div class="kpi-val">${gt}</div><div class="kpi-sub">6 emp × 14 services</div><div class="kpi-ico">∑</div></div>
<div class="kpi-card cb"><div class="kpi-lbl">Team Average</div><div class="kpi-val">${avg}</div><div class="kpi-sub">Per emp per service</div><div class="kpi-ico">≈</div></div>
<div class="kpi-card co"><div class="kpi-lbl">Top Performer</div><div class="kpi-val">${srt[0].name.split(' ')[0]}</div><div class="kpi-sub">Score: ${eTotal(srt[0])}</div><div class="kpi-ico">🏆</div></div>
<div class="kpi-card cr"><div class="kpi-lbl">Needs Support</div><div class="kpi-val">${srt[srt.length-1].name.split(' ')[0]}</div><div class="kpi-sub">Score: ${eTotal(srt[srt.length-1])}</div><div class="kpi-ico">📌</div></div>
<div class="kpi-card cg"><div class="kpi-lbl">Strongest Svc</div><div class="kpi-val">${bS.label}</div><div class="kpi-sub">Total: ${sTotal(bS.key)}</div><div class="kpi-ico">⬆</div></div>
<div class="kpi-card cp"><div class="kpi-lbl">Weakest Svc</div><div class="kpi-val">${wS.label}</div><div class="kpi-sub">Total: ${sTotal(wS.key)}</div><div class="kpi-ico">⬇</div></div>
`;
}
function renderEmp() {
renderEmpKpi();
destroyCharts(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]}">${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">Average</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">Weakest</span><span class="bv">${w.label}</span></div>
</div>
<div class="emp-ch"><canvas id="ec-${emp.id}"></canvas></div>`;
g.appendChild(c);
const col = ECOLS[i % ECOLS.length];
CH.emp[emp.id] = new Chart(c.querySelector(`#ec-${emp.id}`).getContext('2d'), {
type: 'bar',
data: { labels: SERVICES.map(s => s.label),
datasets: [{ data: SERVICES.map(s => emp[s.key] || 0),
backgroundColor: col + '33', borderColor: col,
borderWidth: 2, borderRadius: 4, borderSkipped: false }]
},
options: { ...CB(), indexAxis: 'y', scales: {
x: { beginAtZero: true, max: 100, grid: { color: '#EDF0F4' }, ticks: { font: { size: 10 } } },
y: { grid: { display: false }, ticks: { font: { size: 10 }, color: '#68788E' } }
}}
});
});
}
/* ============================================================
END BLOCK: RENDER — PER EMPLOYEE TAB
============================================================ */
/* ============================================================
BLOCK: RENDER — PER SERVICE TAB
============================================================ */
function renderSvc() {
destroyCharts(CH.svc); CH.svc = {};
// Totals strip
const ts = document.getElementById('svcTotals'); ts.innerHTML = '';
SERVICES.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);
});
// Charts
const g = document.getElementById('svcGrid'); g.innerHTML = '';
SERVICES.forEach(s => {
const emps = EA(), vals = emps.map(e => e[s.key] || 0);
const 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);
CH.svc[s.key] = new Chart(c.querySelector(`#sc-${s.key}`).getContext('2d'), {
type: 'bar',
data: { labels: emps.map(e => e.name.split(' ')[0]),
datasets: [{ data: vals, backgroundColor: ECOLS.map(c => c + '33'), borderColor: ECOLS, borderWidth: 2, borderRadius: 4, borderSkipped: false }]
},
options: { ...CB(), scales: {
y: { beginAtZero: true, max: 100, grid: { color: '#EDF0F4' }, ticks: { font: { size: 10 } } },
x: { grid: { display: false }, ticks: { font: { size: 11 }, color: '#2C3A4E' } }
}}
});
});
}
/* ============================================================
END BLOCK: RENDER — PER SERVICE TAB
============================================================ */
/* ============================================================
BLOCK: RENDER — DISTRIBUTION TAB
============================================================ */
function renderDist() {
destroyCharts(CH.dist); CH.dist = {};
const g = document.getElementById('distGrid'); g.innerHTML = '';
SERVICES.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);
CH.dist[s.key] = new Chart(c.querySelector(`#dc-${s.key}`).getContext('2d'), {
type: 'doughnut',
data: { labels: emps.map(e => e.name.split(' ')[0]),
datasets: [{ data: vals, backgroundColor: ECOLS.map(c => c + 'CC'), borderColor: '#fff', borderWidth: 3 }]
},
options: { ...CB(), cutout: '58%',
plugins: { ...CB().plugins, legend: { display: true, position: 'bottom', labels: { color: '#2C3A4E', font: { size: 11 }, boxWidth: 11, padding: 9 } } }
}
});
});
}
/* ============================================================
END BLOCK: RENDER — DISTRIBUTION TAB
============================================================ */
/* ============================================================
BLOCK: RENDER — DATA ENTRY TAB (form generation)
============================================================ */
function buildEntryForm() {
// Populate employee dropdown
const sel = document.getElementById('entryEmpSelect');
sel.innerHTML = '';
RANKED_EMPLOYEES.forEach(e => {
const opt = document.createElement('option');
opt.value = e.id; opt.textContent = e.name + ' — ' + e.id;
sel.appendChild(opt);
});
// Build score input grid for first employee
buildScoreInputs(RANKED_EMPLOYEES[0].id);
sel.addEventListener('change', () => buildScoreInputs(sel.value));
// Build specialist entry
document.getElementById('specAvEntry').textContent = INI(SPECIALIST.name);
document.getElementById('specNameEntry').textContent = SPECIALIST.name;
document.getElementById('specIdEntry').textContent = SPECIALIST.id + ' · ' + SPECIALIST.role;
// Populate existing specialist values
document.getElementById('spec_processed').value = SPEC_DATA.processed || 0;
document.getElementById('spec_pending').value = SPEC_DATA.pending || 0;
document.getElementById('spec_rejected').value = SPEC_DATA.rejected || 0;
// Build doc scan inputs per service
buildDocScanInputs();
}
function buildScoreInputs(empId) {
const grid = document.getElementById('entryGrid'); grid.innerHTML = '';
const scores = DATA[empId] || {};
SERVICES.forEach(s => {
const div = document.createElement('div'); div.className = 'entry-field';
div.innerHTML = `
<label>${s.label}</label>
<input type="number" id="score_${s.key}" min="0" max="100"
value="${scores[s.key] || 0}" placeholder="0–100">
<span class="entry-hint">Score 0 – 100</span>`;
grid.appendChild(div);
});
}
function buildDocScanInputs() {
const grid = document.getElementById('specDocGrid'); grid.innerHTML = '';
SERVICES.forEach(s => {
const existing = SPEC_DATA.doc_scans[s.key] || { docs: 0, pages: 0 };
const div = document.createElement('div'); div.className = 'spec-entry-field';
div.innerHTML = `
<label>${s.label} — Docs Scanned</label>
<input type="number" id="docs_${s.key}" min="0"
value="${existing.docs || 0}" placeholder="0">`;
grid.appendChild(div);
});
}
/* ============================================================
END BLOCK: RENDER — DATA ENTRY TAB
============================================================ */
/* ============================================================
BLOCK: SAVE — EMPLOYEE SCORES
Called when user clicks "Save & Update Charts"
============================================================ */
function saveEmployeeScores() {
const empId = document.getElementById('entryEmpSelect').value;
if (!DATA[empId]) DATA[empId] = {};
SERVICES.forEach(s => {
const inp = document.getElementById('score_' + s.key);
if (inp) DATA[empId][s.key] = Math.min(100, Math.max(0, parseInt(inp.value) || 0));
});
renderAll();
showToast('✓ ' + RANKED_EMPLOYEES.find(e => e.id === empId).name + ' — scores saved & charts updated', false);
// Flash the button green
const btn = document.getElementById('entrySaveBtn');
btn.classList.add('saved'); btn.textContent = '✓ Saved!';
setTimeout(() => { btn.classList.remove('saved'); btn.textContent = '✓ Save & Update Charts'; }, 2000);
}
/* ============================================================
END BLOCK: SAVE — EMPLOYEE SCORES
============================================================ */
/* ============================================================
BLOCK: SAVE — SPECIALIST DATA
Called when user clicks "Save Specialist Data"
============================================================ */
function saveSpecialistData() {
SPEC_DATA.processed = parseInt(document.getElementById('spec_processed').value) || 0;
SPEC_DATA.pending = parseInt(document.getElementById('spec_pending').value) || 0;
SPEC_DATA.rejected = parseInt(document.getElementById('spec_rejected').value) || 0;
SERVICES.forEach(s => {
const inp = document.getElementById('docs_' + s.key);
if (!SPEC_DATA.doc_scans[s.key]) SPEC_DATA.doc_scans[s.key] = { docs: 0, pages: 0 };
if (inp) SPEC_DATA.doc_scans[s.key].docs = parseInt(inp.value) || 0;
});
renderOA();
showToast('✓ Specialist data saved — Other Actions updated', true);
const btn = document.getElementById('specSaveBtn');
btn.classList.add('saved'); btn.textContent = '✓ Saved!';
setTimeout(() => { btn.classList.remove('saved'); btn.textContent = '✓ Save Specialist Data & Update Charts'; }, 2000);
}
/* ============================================================
END BLOCK: SAVE — SPECIALIST DATA
============================================================ */
/* ============================================================
BLOCK: TOAST NOTIFICATION
============================================================ */
function showToast(msg, isSpec) {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = 'toast' + (isSpec ? ' spec-toast' : '');
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 3000);
}
/* ============================================================
END BLOCK: TOAST NOTIFICATION
============================================================ */
/* ============================================================
BLOCK: RENDER ALL CHARTS
============================================================ */
function renderAll() {
renderInsights();
renderEmp();
renderSvc();
renderDist();
renderOA();
}
/* ============================================================
END BLOCK: RENDER ALL CHARTS
============================================================ */
/* ============================================================
BLOCK: TAB SWITCHING
============================================================ */
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');
});
});
/* ============================================================
END BLOCK: TAB SWITCHING
============================================================ */
/* ============================================================
BLOCK: EXPORT HELPERS
============================================================ */
function showOverlay(msg, sub) { document.getElementById('expOverlay').classList.add('show'); document.getElementById('expMsg').textContent = msg; document.getElementById('expSub').textContent = sub; }
function updateMsg(m) { document.getElementById('expMsg').textContent = m; }
function hideOverlay() { document.getElementById('expOverlay').classList.remove('show'); }
/* ============================================================
END BLOCK: EXPORT HELPERS
============================================================ */
/* ============================================================
BLOCK: PDF EXPORT — WHITE + FOREST GREEN
============================================================ */
async function exportPDF() {
showOverlay('Generating PDF', 'Building report…');
await new Promise(r => setTimeout(r, 200));
try {
const { jsPDF } = window.jspdf;
const pdf = new jsPDF('l', 'mm', 'a4');
const pw = 297, ph = 210;
const OW=[240,243,247], OW2=[225,232,240], INK=[13,24,37], BODY=[44,58,78], MUTED=[104,120,142], RULE=[175,192,210];
const G1=[10,92,56], G2=[19,128,80], G3=[29,173,108];
const S_H=[10,128,72], S_M=[181,125,0], S_L=[196,48,48];
const EC=[[26,92,180],[19,128,80],[104,54,208],[217,91,10],[181,125,0],[14,138,138]];
const arr = EA(), gt = GT(), sorted = [...arr].sort((a, b) => eTotal(b) - eTotal(a));
const chrome = (title, sub) => {
pdf.setFillColor(...OW); pdf.rect(0,0,pw,ph,'F');
pdf.setFillColor(...G1); pdf.rect(0,0,pw,8,'F');
pdf.setFillColor(...G3); pdf.rect(0,8,pw,1.2,'F');
pdf.setFillColor(...OW2); pdf.rect(0,ph-9,pw,9,'F');
pdf.setDrawColor(...RULE); pdf.setLineWidth(.3); pdf.line(0,ph-9,pw,ph-9);
pdf.setTextColor(...MUTED); pdf.setFontSize(8); pdf.setFont('helvetica','normal');
pdf.text('HR Performance Analytics · Confidential', 12, ph-3.8);
pdf.text(`Page ${pdf.getCurrentPageInfo().pageNumber}`, pw-12, ph-3.8, {align:'right'});
if (title) { pdf.setTextColor(...INK); pdf.setFontSize(18); pdf.setFont('helvetica','bold'); pdf.text(title, 12, 20); }
if (sub) { pdf.setTextColor(...MUTED); pdf.setFontSize(10); pdf.setFont('helvetica','normal'); pdf.text(sub, 12, 29); }
pdf.setDrawColor(...G3); pdf.setLineWidth(.6); pdf.line(12,32,pw-12,32);
};
// Cover page
pdf.setFillColor(...OW); pdf.rect(0,0,pw,ph,'F');
pdf.setFillColor(...G1); pdf.rect(0,0,pw,58,'F');
pdf.setFillColor(...G2); pdf.rect(0,58,pw,2.5,'F');
pdf.setFillColor(...G3); pdf.rect(0,60.5,pw,.8,'F');
pdf.setFillColor(...G2); pdf.rect(0,0,4,ph,'F');
pdf.setTextColor(196,240,220); pdf.setFontSize(8.5); pdf.setFont('helvetica','bold');
pdf.text('HUMAN RESOURCES · PERFORMANCE ANALYTICS', pw/2+2, 12, {align:'center'});
pdf.setTextColor(255,255,255); pdf.setFontSize(32);
pdf.text('Employee Performance', pw/2+2, 33, {align:'center'});
pdf.setFontSize(26); pdf.setFont('helvetica','normal');
pdf.text('Dashboard Report', pw/2+2, 48, {align:'center'});
pdf.setTextColor(...MUTED); pdf.setFontSize(10); pdf.setFont('helvetica','normal');
pdf.text(new Date().toLocaleDateString('en-GB',{weekday:'long',year:'numeric',month:'long',day:'numeric'}), pw/2+2, 73, {align:'center'});
pdf.setDrawColor(...RULE); pdf.setLineWidth(.4); pdf.line(50,77,pw-50,77);
pdf.setFontSize(9.5);
pdf.text(`6 Ranked Employees · 14 Services · Grand Total: ${gt} · + 1 Specialist`, pw/2+2, 85, {align:'center'});
// KPI boxes
[{l:'Grand Total',v:String(gt),g:G1},{l:'Team Avg',v:(gt/(arr.length*SERVICES.length)).toFixed(1),g:G2},{l:'Top Performer',v:sorted[0].name.split(' ')[0],g:G1},{l:'Efficiency',v:((gt/(arr.length*SERVICES.length*100))*100).toFixed(1)+'%',g:G2}].forEach((k,i)=>{
const x=14+i*68, y=96;
pdf.setFillColor(...OW2); pdf.roundedRect(x,y,60,34,2,2,'F');
pdf.setDrawColor(...k.g); pdf.setLineWidth(.8); pdf.roundedRect(x,y,60,34,2,2,'S');
pdf.setFillColor(...k.g); pdf.roundedRect(x,y,60,5,2,2,'F'); pdf.rect(x,y+2.5,60,2.5,'F');
pdf.setTextColor(...MUTED); pdf.setFontSize(7.5); pdf.setFont('helvetica','normal'); pdf.text(k.l,x+30,y+13,{align:'center'});
pdf.setTextColor(...k.g); pdf.setFontSize(18); pdf.setFont('helvetica','bold'); pdf.text(String(k.v),x+30,y+27,{align:'center'});
});
// Ranking strip
const GN=[196,240,220];
pdf.setFillColor(...GN); pdf.roundedRect(14,139,pw-28,57,2,2,'F');
pdf.setFillColor(...G2); pdf.roundedRect(14,139,pw-28,7,2,2,'F'); pdf.rect(14,143,pw-28,3,'F');
pdf.setTextColor(255,255,255); pdf.setFontSize(9); pdf.setFont('helvetica','bold'); pdf.text('TOP PERFORMERS — 6 EMPLOYEES', pw/2, 144.5, {align:'center'});
sorted.forEach((emp,i)=>{
const rx=20, ry=153+i*7.3, rgb=EC[arr.findIndex(a=>a.id===emp.id)%EC.length];
pdf.setFillColor(...rgb); pdf.circle(rx+1,ry-1.2,1.8,'F');
pdf.setTextColor(...MUTED); pdf.setFontSize(7.5); pdf.setFont('helvetica','normal');
pdf.text(i===0?'1st':i===1?'2nd':i===2?'3rd':`#${i+1}`, rx+5, ry);
pdf.setTextColor(...BODY); pdf.text(emp.name, rx+20, ry);
const bx=rx+90, bw=145, pct=eTotal(emp)/(eTotal(sorted[0])||1);
pdf.setFillColor(...RULE); pdf.roundedRect(bx,ry-3.2,bw,3.8,.5,.5,'F');
pdf.setFillColor(...rgb); pdf.roundedRect(bx,ry-3.2,bw*pct,3.8,.5,.5,'F');
pdf.setTextColor(...rgb); pdf.setFontSize(8); pdf.setFont('helvetica','bold'); pdf.text(String(eTotal(emp)),bx+bw+4,ry);
});
pdf.setFillColor(240,234,255); pdf.roundedRect(14,200,pw-28,6,1,1,'F');
pdf.setTextColor(104,54,208); pdf.setFontSize(7.5); pdf.setFont('helvetica','bold');
pdf.text(`Specialist: ${SPECIALIST.name} (${SPECIALIST.id}) — ${SPECIALIST.role} · IT Processed: ${SPEC_DATA.processed||0}`, pw/2, 203.8, {align:'center'});
// Employee table
pdf.addPage(); chrome('Employee Performance Summary — 6 Ranked Employees','Scores across all 14 HR services');
const cols=['#','Employee','ID',...SERVICES.map(s=>s.label.slice(0,8)),'Total','Avg'];
const colW=[9,34,15,...SERVICES.map(()=>13),15,12];
const sx=10, hy=36;
pdf.setFillColor(...G1); pdf.rect(sx,hy,pw-20,8,'F');
pdf.setTextColor(255,255,255); pdf.setFontSize(7); pdf.setFont('helvetica','bold');
let cx=sx; cols.forEach((c,i)=>{pdf.text(c,cx+colW[i]/2,hy+5.5,{align:'center'});cx+=colW[i];});
sorted.forEach((emp,ri)=>{
const rowY=hy+8+ri*10;
pdf.setFillColor(...(ri%2===0?GN:OW)); pdf.rect(sx,rowY,pw-20,10,'F');
pdf.setDrawColor(...RULE); pdf.setLineWidth(.15); pdf.rect(sx,rowY,pw-20,10,'S');
const cells=[`#${ri+1}`,emp.name,emp.id,...SERVICES.map(s=>String(emp[s.key]||0)),String(eTotal(emp)),eAvg(emp).toFixed(1)];
cx=sx; pdf.setFontSize(7);
cells.forEach((cell,ci)=>{
if(ci===0){const mc=ri===0?G1:ri===1?[120,120,120]:ri===2?[180,100,30]:MUTED;pdf.setTextColor(...mc);pdf.setFont('helvetica','bold');}
else if(ci===cols.length-2){pdf.setTextColor(...G1);pdf.setFont('helvetica','bold');}
else if(ci===cols.length-1){pdf.setTextColor(26,92,180);pdf.setFont('helvetica','bold');}
else if(ci>2){const v=parseFloat(cell)||0;pdf.setTextColor(...(v>=80?S_H:v>=60?S_M:S_L));pdf.setFont('helvetica','normal');}
else{pdf.setTextColor(...BODY);pdf.setFont('helvetica','normal');}
pdf.text(cell,cx+colW[ci]/2,rowY+6.5,{align:'center'}); cx+=colW[ci];
});
});
// Service charts
updateMsg('Rendering service charts…');
for (let si=0; si<SERVICES.length; si+=2) {
pdf.addPage(); chrome('Service Performance Comparison','Employee scores per service — 0 to 100 scale');
for (let ci=0; ci<2 && si+ci<SERVICES.length; ci++) {
const svc=SERVICES[si+ci], emps=EA(), vals=emps.map(e=>e[svc.key]||0);
const ox=10+ci*140, oy=36, cW=133, cH=148;
pdf.setFillColor(...OW2); pdf.roundedRect(ox,oy,cW,cH,2,2,'F');
pdf.setDrawColor(...RULE); pdf.setLineWidth(.3); pdf.roundedRect(ox,oy,cW,cH,2,2,'S');
pdf.setFillColor(...G1); pdf.roundedRect(ox,oy,cW,8,2,2,'F'); pdf.rect(ox,oy+4,cW,4,'F');
pdf.setTextColor(255,255,255); pdf.setFontSize(8.5); pdf.setFont('helvetica','bold'); pdf.text(svc.label,ox+cW/2,oy+5.8,{align:'center'});
pdf.setFillColor(...GN); pdf.rect(ox,oy+8,cW,7,'F');
pdf.setTextColor(...G1); pdf.setFontSize(7.5); pdf.setFont('helvetica','bold');
pdf.text(`TOTAL: ${sTotal(svc.key)}`,ox+cW*.26,oy+13,{align:'center'});
pdf.text(`AVG: ${sAvg(svc.key).toFixed(1)}`,ox+cW*.76,oy+13,{align:'center'});
const bax=ox+32, bay=oy+20, mxV=Math.max(...vals,1), slot=cH/(emps.length+1.5);
emps.forEach((emp,ei)=>{
const v=vals[ei], pct=v/mxV, rgb=EC[ei%EC.length];
const by=bay+slot*(ei+0.5), bh=Math.max(slot*.5,3), bw=Math.max(pct*(cW-38),.5);
pdf.setFillColor(...RULE); pdf.roundedRect(bax,by-bh/2,cW-38,bh,.4,.4,'F');
pdf.setFillColor(...rgb); pdf.roundedRect(bax,by-bh/2,bw,bh,.4,.4,'F');
pdf.setTextColor(...MUTED); pdf.setFontSize(6.5); pdf.setFont('helvetica','normal'); pdf.text(emp.name.split(' ')[0],bax-2,by+2,{align:'right'});
pdf.setTextColor(...rgb); pdf.setFontSize(7); pdf.setFont('helvetica','bold'); pdf.text(String(v),bax+bw+2,by+2);
});
}
await new Promise(r=>setTimeout(r,0));
}
// Leaderboard
pdf.addPage(); chrome('Performance Leaderboard — 6 Employees','Ranked by cumulative score across all 14 services');
sorted.forEach((emp,i)=>{
const rowY=36+i*24, t=eTotal(emp), pct=t/(eTotal(sorted[0])||1);
const rgb=EC[arr.findIndex(a=>a.id===emp.id)%EC.length];
pdf.setFillColor(...(i%2===0?GN:OW)); pdf.roundedRect(10,rowY,pw-20,20,1.5,1.5,'F');
pdf.setDrawColor(...RULE); pdf.setLineWidth(.2); pdf.roundedRect(10,rowY,pw-20,20,1.5,1.5,'S');
pdf.setFillColor(...rgb); pdf.roundedRect(10,rowY,3.5,20,1,1,'F');
const mc=i===0?G1:i===1?[120,120,120]:i===2?[180,100,30]:MUTED;
pdf.setTextColor(...mc); pdf.setFontSize(11); pdf.setFont('helvetica','bold');
pdf.text(i===0?'1ST':i===1?'2ND':i===2?'3RD':`#${i+1}`,20,rowY+13);
pdf.setTextColor(...INK); pdf.setFontSize(11.5); pdf.setFont('helvetica','bold'); pdf.text(emp.name,40,rowY+7.5);
pdf.setTextColor(...MUTED); pdf.setFontSize(8.5); pdf.setFont('helvetica','normal');
pdf.text(`Avg: ${eAvg(emp).toFixed(1)} Best: ${eBest(emp).label} Weakest: ${eWorst(emp).label}`,40,rowY+15);
const bx=148, by=rowY+7, bw=115, bh=5.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(11); pdf.setFont('helvetica','bold'); pdf.text(String(t),bx+bw+5,rowY+13);
});
// Specialist row on leaderboard page
const sRowY=36+sorted.length*24+4;
pdf.setFillColor(240,234,255); pdf.roundedRect(10,sRowY,pw-20,20,1.5,1.5,'F');
pdf.setDrawColor(124,58,237); pdf.setLineWidth(.4); pdf.roundedRect(10,sRowY,pw-20,20,1.5,1.5,'S');
pdf.setFillColor(124,58,237); pdf.roundedRect(10,sRowY,3.5,20,1,1,'F');
pdf.setTextColor(104,54,208); pdf.setFontSize(9); pdf.setFont('helvetica','bold'); pdf.text('SPECIALIST — NOT RANKED',20,sRowY+8);
pdf.setTextColor(...BODY); pdf.setFontSize(10); pdf.text(SPECIALIST.name,40,sRowY+8);
pdf.setTextColor(...MUTED); pdf.setFontSize(8); pdf.setFont('helvetica','normal');
const procCount=SPEC_DATA.processed||0, totalDocs=Object.values(SPEC_DATA.doc_scans).reduce((s,d)=>s+(parseInt(d.docs)||0),0);
pdf.text(`${SPECIALIST.role} · IT Requests Processed: ${procCount} · Total Docs Scanned: ${totalDocs}`,40,sRowY+16);
pdf.save('HR_Performance_Report.pdf');
hideOverlay();
} catch(err) { console.error(err); hideOverlay(); alert('PDF export failed: ' + err.message); }
}
/* ============================================================
END BLOCK: PDF EXPORT
============================================================ */
/* ============================================================
BLOCK: PPTX EXPORT — WHITE + FOREST GREEN
============================================================ */
async function exportPPTX() {
showOverlay('Generating PowerPoint', 'Building slides…');
await new Promise(r => setTimeout(r, 200));
try {
const pres = new PptxGenJS();
pres.layout = 'LAYOUT_WIDE'; pres.title = 'HR Performance Dashboard';
const BG='F0F3F7', LT='E5EBF2', GN='C2F0DC';
const INK='0D1825', MUTED='68788E', RULE='B0C0D0';
const G1='0A5C38', G2='138050', G3='1DAD6C', G4='C2F0DC';
const RED='C43030', AMBER='B57D00', BLUE='1A5CB4', PURPLE='6836D0', TEAL='0E8A8A';
const SPEC='7C3AED';
const EAC=[BLUE,G2,PURPLE,'D95B0A',AMBER,TEAL];
const arr=EA(), gt=GT(), sorted=[...arr].sort((a,b)=>eTotal(b)-eTotal(a));
const empNames=arr.map(e=>e.name.split(' ')[0]);
const sh=(sl,ac,title,sub)=>{
sl.background={color:BG};
sl.addShape(pres.shapes.RECTANGLE,{x:0,y:0,w:13.3,h:.14,fill:{color:ac}});
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:7,color:MUTED,fontFace:'Calibri'});
if(title) sl.addText(title,{x:.4,y:.22,w:10,h:.6,fontSize:22,bold:true,color:INK,fontFace:'Calibri'});
if(sub) sl.addText(sub,{x:.4,y:.8,w:12.5,h:.28,fontSize:10,color:MUTED,fontFace:'Calibri'});
};
// Cover slide
let sl=pres.addSlide(); sl.background={color:BG};
sl.addShape(pres.shapes.RECTANGLE,{x:0,y:0,w:.22,h:7.5,fill:{color:G1}});
sl.addShape(pres.shapes.RECTANGLE,{x:.22,y:0,w:13.08,h:2.35,fill:{color:LT}});
sl.addText('HR PERFORMANCE ANALYTICS',{x:.5,y:.36,w:12.5,h:.46,fontSize:12,bold:true,color:G1,charSpacing:4,fontFace:'Calibri'});
sl.addShape(pres.shapes.LINE,{x:.5,y:.92,w:12.4,h:0,line:{color:RULE,width:.6}});
sl.addText('Performance Dashboard',{x:.5,y:1.04,w:12.4,h:.96,fontSize:44,bold:true,color:INK,fontFace:'Calibri'});
sl.addText(`Generated ${new Date().toLocaleDateString('en-GB',{year:'numeric',month:'long',day:'numeric'})}`,{x:.5,y:2.5,w:12.4,h:.38,fontSize:12,color:MUTED,fontFace:'Calibri'});
sl.addShape(pres.shapes.LINE,{x:.5,y:2.94,w:12.4,h:0,line:{color:RULE,width:.5}});
[{l:'Grand Total',v:String(gt),c:G1},{l:'Team Avg',v:(gt/(arr.length*SERVICES.length)).toFixed(1),c:G2},{l:'Ranked',v:'6',c:BLUE},{l:'Services',v:'14',c:PURPLE}].forEach((k,i)=>{
const x=.5+i*3.2, y=3.15;
sl.addShape(pres.shapes.RECTANGLE,{x,y,w:3.0,h:1.68,fill:{color:G4},line:{color:RULE,width:.4}});
sl.addShape(pres.shapes.RECTANGLE,{x,y,w:3.0,h:.14,fill:{color:k.c}});
sl.addText(k.l,{x,y:y+.2,w:3.0,h:.32,fontSize:9,color:MUTED,align:'center',fontFace:'Calibri'});
sl.addText(k.v,{x,y:y+.54,w:3.0,h:.86,fontSize:34,bold:true,color:k.c,align:'center',fontFace:'Calibri'});
});
sl.addShape(pres.shapes.RECTANGLE,{x:.5,y:5.0,w:12.5,h:1.92,fill:{color:LT}});
sl.addText('Top Performers',{x:.65,y:5.1,w:5,h:.3,fontSize:10,bold:true,color:INK,fontFace:'Calibri'});
sorted.forEach((emp,i)=>{
const ei=arr.findIndex(a=>a.id===emp.id), col=EAC[ei%EAC.length], ry=5.5+i*.3;
sl.addShape(pres.shapes.RECTANGLE,{x:.65,y:ry,w:.08,h:.22,fill:{color:col}});
sl.addText(`${i===0?'1st':i===1?'2nd':i===2?'3rd':'#'+(i+1)} ${emp.name}`,{x:.8,y:ry,w:4.5,h:.26,fontSize:9.5,color:INK,fontFace:'Calibri'});
const bx=5.5, bw=6.8, pct=eTotal(emp)/(eTotal(sorted[0])||1);
sl.addShape(pres.shapes.RECTANGLE,{x:bx,y:ry+.05,w:bw,h:.12,fill:{color:RULE}});
sl.addShape(pres.shapes.RECTANGLE,{x:bx,y:ry+.05,w:bw*pct,h:.12,fill:{color:col}});
sl.addText(String(eTotal(emp)),{x:bx+bw+.1,y:ry,w:.7,h:.26,fontSize:9.5,bold:true,color:col,fontFace:'Calibri'});
});
sl.addShape(pres.shapes.RECTANGLE,{x:.5,y:6.98,w:12.5,h:.44,fill:{color:'F0EAFF'},line:{color:'DDD0FF',width:.4}});
sl.addText(`Specialist (Not Ranked): ${SPECIALIST.name} — ${SPECIALIST.role}`,{x:.65,y:7.02,w:12.2,h:.34,fontSize:9.5,color:SPEC,italic:true,fontFace:'Calibri'});
// Employee summary table
updateMsg('Building table…');
sl=pres.addSlide(); sh(sl,G1,'Employee Performance Summary','6 Ranked Employees · 14 Services');
const tC=['#','Name','ID',...SERVICES.map(s=>s.label.slice(0,8)),'Total','Avg'];
const tW=[.28,.88,.57,...SERVICES.map(()=>.56),.58,.5];
const tRows=[[tC.map(c=>({text:c,options:{bold:true,color:'FFFFFF',fontSize:6.5,align:'center',fill:{color:G1},border:{pt:.5,color:RULE}}}))]];
sorted.forEach((emp,ri)=>{
const rf=ri%2===0?BG:GN, row=[];
row.push({text:`#${ri+1}`,options:{bold:true,color:ri===0?AMBER:ri===1?MUTED:ri===2?'8C5000':MUTED,fontSize:6.5,align:'center',fill:{color:rf},border:{pt:.3,color:RULE}}});
row.push({text:emp.name,options:{color:INK,fontSize:7,fill:{color:rf},border:{pt:.3,color:RULE}}});
row.push({text:emp.id,options:{color:MUTED,fontSize:6.5,align:'center',fill:{color:rf},border:{pt:.3,color:RULE}}});
SERVICES.forEach(s=>{const v=emp[s.key]||0;row.push({text:String(v),options:{color:v>=80?G1:v>=60?AMBER:RED,fontSize:7,align:'center',fill:{color:rf},border:{pt:.3,color:RULE}}});});
row.push({text:String(eTotal(emp)),options:{bold:true,color:G1,fontSize:8,align:'center',fill:{color:rf},border:{pt:.3,color:RULE}}});
row.push({text:eAvg(emp).toFixed(1),options:{bold:true,color:BLUE,fontSize:7,align:'center',fill:{color:rf},border:{pt:.3,color:RULE}}});
tRows.push(row);
});
sl.addTable(tRows,{x:.3,y:1.12,w:12.7,colW:tW,rowH:.41,border:{pt:.3,color:RULE}});
// Service bar charts
updateMsg('Adding service charts…');
for(let si=0;si<SERVICES.length;si+=2){
sl=pres.addSlide(); sh(sl,G2,'Service Performance Comparison','Employee scores — 0 to 100 scale');
for(let ci=0;ci<2&&si+ci<SERVICES.length;ci++){
const svc=SERVICES[si+ci], ox=.3+ci*6.65, oy=1.08, cw=6.2;
const vals=arr.map(e=>e[svc.key]||0);
sl.addShape(pres.shapes.RECTANGLE,{x:ox,y:oy,w:cw,h:.54,fill:{color:LT},line:{color:RULE,width:.4}});
sl.addShape(pres.shapes.RECTANGLE,{x:ox,y:oy,w:.12,h:.54,fill:{color:G1}});
sl.addText(svc.label,{x:ox+.18,y:oy+.07,w:cw*.55,h:.4,fontSize:12,bold:true,color:INK,fontFace:'Calibri'});
sl.addText(`Total: ${sTotal(svc.key)} · Avg: ${sAvg(svc.key).toFixed(1)}`,{x:ox+cw*.5,y:oy+.14,w:cw*.48,h:.28,fontSize:9,color:MUTED,align:'right',fontFace:'Calibri'});
sl.addChart(pres.charts.BAR,[{name:svc.label,labels:empNames,values:vals}],{x:ox,y:oy+.56,w:cw,h:5.6,barDir:'col',chartColors:EAC,chartArea:{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:8,showLegend:false});
}
await new Promise(r=>setTimeout(r,0));
}
// Distribution charts
updateMsg('Adding distribution…');
for(let si=0;si<SERVICES.length;si+=3){
sl=pres.addSlide(); sh(sl,PURPLE,'Service Distribution','Score share per employee');
for(let ci=0;ci<3&&si+ci<SERVICES.length;ci++){
const svc=SERVICES[si+ci], ox=.25+ci*4.36, oy=1.08, cw=4.1;
const vals=arr.map(e=>e[svc.key]||0);
sl.addShape(pres.shapes.RECTANGLE,{x:ox,y:oy,w:cw,h:.48,fill:{color:LT},line:{color:RULE,width:.4}});
sl.addShape(pres.shapes.RECTANGLE,{x:ox,y:oy,w:.12,h:.48,fill:{color:PURPLE}});
sl.addText(svc.label,{x:ox+.16,y:oy+.09,w:cw-.22,h:.32,fontSize:10,bold:true,color:INK,fontFace:'Calibri'});
sl.addChart(pres.charts.DOUGHNUT,[{name:svc.label,labels:empNames,values:vals}],{x:ox,y:oy+.5,w:cw,h:5.78,holeSize:55,chartColors:EAC,chartArea:{fill:{color:BG}},dataLabelFontSize:7.5,dataLabelColor:INK,showPercent:true,legendPos:'b',showLegend:true,legendFontSize:8,legendColor:MUTED});
}
await new Promise(r=>setTimeout(r,0));
}
// Leaderboard slide
updateMsg('Building leaderboard…');
sl=pres.addSlide(); sh(sl,AMBER,'Performance Leaderboard','6 Employees ranked by cumulative score');
sl.addText(`Grand Total: ${gt}`,{x:9.5,y:.22,w:3.5,h:.56,fontSize:15,bold:true,color:G1,align:'right',fontFace:'Calibri'});
sorted.forEach((emp,i)=>{
const ry=1.14+i*.8, t=eTotal(emp), pct=t/(eTotal(sorted[0])||1);
const ei=arr.findIndex(a=>a.id===emp.id), col=EAC[ei%EAC.length];
sl.addShape(pres.shapes.RECTANGLE,{x:.3,y:ry,w:12.7,h:.68,fill:{color:i%2===0?GN:BG},line:{color:RULE,width:.3}});
sl.addShape(pres.shapes.RECTANGLE,{x:.3,y:ry,w:.14,h:.68,fill:{color:col}});
sl.addText(i===0?'🥇':i===1?'🥈':i===2?'🥉':`#${i+1}`,{x:.5,y:ry+.07,w:.64,h:.52,fontSize:15,align:'center',fontFace:'Segoe UI Emoji'});
sl.addText(emp.name,{x:1.25,y:ry+.06,w:4,h:.28,fontSize:12,bold:true,color:INK,fontFace:'Calibri'});
sl.addText(`Avg: ${eAvg(emp).toFixed(1)} · Best: ${eBest(emp).label} · Weakest: ${eWorst(emp).label}`,{x:1.25,y:ry+.38,w:5.4,h:.24,fontSize:8,color:MUTED,fontFace:'Calibri'});
const bx=6.9, bw=5.4;
sl.addShape(pres.shapes.RECTANGLE,{x:bx,y:ry+.21,w:bw,h:.26,fill:{color:RULE}});
if(pct>0) sl.addShape(pres.shapes.RECTANGLE,{x:bx,y:ry+.21,w:bw*pct,h:.26,fill:{color:col}});
sl.addText(String(t),{x:bx+bw+.12,y:ry+.12,w:.85,h:.42,fontSize:14,bold:true,color:col,fontFace:'Calibri'});
});
// Specialist row
const sry=1.14+sorted.length*.8+.1;
sl.addShape(pres.shapes.RECTANGLE,{x:.3,y:sry,w:12.7,h:.68,fill:{color:'F0EAFF'},line:{color:'DDD0FF',width:.4}});
sl.addShape(pres.shapes.RECTANGLE,{x:.3,y:sry,w:.14,h:.68,fill:{color:SPEC}});
sl.addText('★',{x:.5,y:sry+.07,w:.64,h:.52,fontSize:15,align:'center',color:SPEC,fontFace:'Calibri'});
sl.addText(SPECIALIST.name,{x:1.25,y:sry+.06,w:4,h:.28,fontSize:12,bold:true,color:INK,fontFace:'Calibri'});
const procC=SPEC_DATA.processed||0, totDocC=Object.values(SPEC_DATA.doc_scans).reduce((s,d)=>s+(parseInt(d.docs)||0),0);
sl.addText(`Specialist — Not Ranked · IT Processed: ${procC} · Docs Scanned: ${totDocC}`,{x:1.25,y:sry+.38,w:8,h:.24,fontSize:8,color:SPEC,fontFace:'Calibri'});
// Insights slide
updateMsg('Adding insights…');
sl=pres.addSlide(); sh(sl,G1,'Performance Insights','Total scores and gap analysis — 6 employees');
sl.addShape(pres.shapes.RECTANGLE,{x:.3,y:1.02,w:6.1,h:.44,fill:{color:LT},line:{color:RULE,width:.3}});
sl.addShape(pres.shapes.RECTANGLE,{x:.3,y:1.02,w:.12,h:.44,fill:{color:BLUE}});
sl.addText('Total Score per Employee',{x:.48,y:1.09,w:5.9,h:.3,fontSize:10,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.48,w:6.1,h:5.8,barDir:'col',chartColors:sorted.map(e=>EAC[arr.findIndex(a=>a.id===e.id)%EAC.length]),chartArea:{fill:{color:BG}},catAxisLabelColor:MUTED,valAxisLabelColor:MUTED,valGridLine:{color:RULE,size:.4},catGridLine:{style:'none'},showValue:true,dataLabelColor:INK,dataLabelFontSize:9,showLegend:false});
const gaps=SERVICES.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.02,w:6.1,h:.44,fill:{color:LT},line:{color:RULE,width:.3}});
sl.addShape(pres.shapes.RECTANGLE,{x:6.9,y:1.02,w:.12,h:.44,fill:{color:AMBER}});
sl.addText('Performance Gap (Max − Min)',{x:7.08,y:1.09,w:5.9,h:.3,fontSize:10,bold:true,color:INK,fontFace:'Calibri'});
sl.addChart(pres.charts.BAR,[{name:'Gap',labels:SERVICES.map(s=>s.label),values:gaps}],{x:6.9,y:1.48,w:6.1,h:5.8,barDir:'bar',chartColors:gaps.map(g=>g>40?RED:g>20?AMBER:G2),chartArea:{fill:{color:BG}},catAxisLabelColor:MUTED,valAxisLabelColor:MUTED,valGridLine:{color:RULE,size:.4},catGridLine:{style:'none'},showValue:true,dataLabelColor:INK,dataLabelFontSize:8,showLegend:false});
// Team analytics slide
sl=pres.addSlide(); sh(sl,G2,'Team Analytics','Avg scores and service volume');
sl.addShape(pres.shapes.RECTANGLE,{x:.3,y:1.02,w:6.1,h:.44,fill:{color:LT},line:{color:RULE,width:.3}});
sl.addShape(pres.shapes.RECTANGLE,{x:.3,y:1.02,w:.12,h:.44,fill:{color:G2}});
sl.addText('Average Score per Employee',{x:.48,y:1.09,w:5.9,h:.3,fontSize:10,bold:true,color:INK,fontFace:'Calibri'});
sl.addChart(pres.charts.LINE,[{name:'Avg',labels:arr.map(e=>e.name.split(' ')[0]),values:arr.map(e=>parseFloat(eAvg(e).toFixed(1)))}],{x:.3,y:1.48,w:6.1,h:5.8,lineSize:2.5,lineSmooth:true,chartColors:[G1],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:9,showLegend:false});
sl.addShape(pres.shapes.RECTANGLE,{x:6.9,y:1.02,w:6.1,h:.44,fill:{color:LT},line:{color:RULE,width:.3}});
sl.addShape(pres.shapes.RECTANGLE,{x:6.9,y:1.02,w:.12,h:.44,fill:{color:PURPLE}});
sl.addText('Service Volume Distribution',{x:7.08,y:1.09,w:5.9,h:.3,fontSize:10,bold:true,color:INK,fontFace:'Calibri'});
sl.addChart(pres.charts.PIE,[{name:'Vol',labels:SERVICES.map(s=>s.label),values:SERVICES.map(s=>sTotal(s.key))}],{x:6.9,y:1.48,w:6.1,h:5.8,chartColors:EAC,chartArea:{fill:{color:BG}},showPercent:true,dataLabelColor:INK,dataLabelFontSize:8,legendPos:'r',showLegend:true,legendFontSize:9,legendColor:MUTED});
// Specialist slide
updateMsg('Adding specialist data…');
sl=pres.addSlide(); sh(sl,SPEC,'Other Actions — Specialist Employee',`${SPECIALIST.name} · IT Requests & Documents Scanned`);
[{l:'IT Processed',v:String(SPEC_DATA.processed||0),c:G1},{l:'Pending',v:String(SPEC_DATA.pending||0),c:AMBER},{l:'Rejected',v:String(SPEC_DATA.rejected||0),c:RED},{l:'Docs Scanned',v:String(totDocC),c:BLUE}].forEach((k,i)=>{
const x=.5+i*3.2, y=1.08;
sl.addShape(pres.shapes.RECTANGLE,{x,y,w:3.0,h:1.6,fill:{color:'F0EAFF'},line:{color:'DDD0FF',width:.4}});
sl.addShape(pres.shapes.RECTANGLE,{x,y,w:3.0,h:.14,fill:{color:k.c}});
sl.addText(k.l,{x,y:y+.2,w:3.0,h:.3,fontSize:9,color:MUTED,align:'center',fontFace:'Calibri'});
sl.addText(k.v,{x,y:y+.52,w:3.0,h:.78,fontSize:28,bold:true,color:k.c,align:'center',fontFace:'Calibri'});
});
sl.addShape(pres.shapes.RECTANGLE,{x:.3,y:2.85,w:12.7,h:.42,fill:{color:LT}});
sl.addShape(pres.shapes.RECTANGLE,{x:.3,y:2.85,w:.12,h:.42,fill:{color:TEAL}});
sl.addText('Documents Scanned per HR Service',{x:.5,y:2.91,w:12.4,h:.28,fontSize:10,bold:true,color:INK,fontFace:'Calibri'});
const dsL=SERVICES.map(s=>s.label);
const dsV=SERVICES.map(s=>parseInt((SPEC_DATA.doc_scans[s.key]||{}).docs)||0);
sl.addChart(pres.charts.BAR,[{name:'Docs',labels:dsL,values:dsV}],{x:.3,y:3.3,w:12.7,h:3.88,barDir:'col',chartColors:EAC,chartArea:{fill:{color:BG}},catAxisLabelColor:MUTED,valAxisLabelColor:MUTED,valGridLine:{color:RULE,size:.4},catGridLine:{style:'none'},showValue:true,dataLabelColor:INK,dataLabelFontSize:8,showLegend:false});
await pres.writeFile({ fileName: 'HR_Performance_Dashboard.pptx' });
hideOverlay();
} catch(err) { console.error(err); hideOverlay(); alert('PPTX export failed: ' + err.message); }
}
/* ============================================================
END BLOCK: PPTX EXPORT
============================================================ */
/* ============================================================
BLOCK: EXPORT BOTH (PDF + PPTX sequentially)
============================================================ */
async function exportBoth() {
await exportPDF();
await new Promise(r => setTimeout(r, 300));
await exportPPTX();
}
/* ============================================================
END BLOCK: EXPORT BOTH
============================================================ */
/* ============================================================
BLOCK: INITIALISATION
Runs once when the page loads.
Builds the entry form and renders all charts.
============================================================ */
buildEntryForm();
renderAll();
/* ============================================================
END BLOCK: INITIALISATION
============================================================ */
</script>
<!-- ============================================================
END BLOCK: JAVASCRIPT — DATA CONFIG
============================================================ -->
</body>
</html>
✓ Scores saved — charts updated
Performance Analytics
Human Resources Dashboard
Performance Intelligence — 6 Ranked Employees
🏆 Overall Ranking — 6 Employees
📡Team Average by Service
Individual Radar Profiles
Comparative Analysis
KPI Overview — 6 Ranked Employees
Employee Performance Cards
Service Totals
Service Comparison — 6 Employees
Score Distribution by Service
Specialist Employee — Internal Transfer Focus
?
Specialist Employee
—
Total IT Requests Processed
0
Enter data via Data Entry tab
Processed
0
Pending
0
Rejected
0
Docs Scanned
0
Total Pages Scanned
0
🗂️Documents Scanned per HR Service
Manual Data Entry
Select an employee from the dropdown, enter their scores for each service (0–100),
then click Save & Update Charts. All charts and rankings update instantly.
Use the Specialist section at the bottom to enter Internal Transfer and document scan data.
Ranked Employee Scores (0 – 100 per service)
Specialist Employee — Internal Transfer & Document Data
?
Specialist Employee
—
Internal Transfer Requests
Documents Scanned per Service
Comments
Post a Comment