Pinnacle/site_dump.html
2026-07-29 00:22:15 +05:00

3961 lines
272 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pinnacle Decision Portal</title>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&family=Source+Sans+3:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js"></script>
<!-- EmailJS -->
<script src="https://cdn.jsdelivr.net/npm/@emailjs/browser@4/dist/email.min.js"></script>
<script>
function initEmailJS(){
var pk = DB && DB._ejPk;
if(pk && window.emailjs){ try{ emailjs.init({publicKey: pk}); }catch(e){} }
}
</script>
<style>
:root{--navy:#0d2137;--nm:#1a3a5c;--nl:#2e5f8a;--gold:#c9a227;--gl:#e8c660;--gp:#f5e9c0;--off:#f8f6f0;--g50:#fafafa;--g100:#f4f4f5;--g200:#e4e4e7;--g300:#d1d5db;--g400:#a1a1aa;--g600:#52525b;--g800:#27272a;--success:#15803d;--sbg:#dcfce7;--warn:#b45309;--wbg:#fef3c7;--danger:#b91c1c;--dbg:#fee2e2;--info:#1d4ed8;--ibg:#dbeafe;--sw:255px;--hh:62px;--r:8px;--sh:0 1px 3px rgba(0,0,0,.12);--shm:0 4px 16px rgba(13,33,55,.18);--shl:0 10px 32px rgba(13,33,55,.22)}
*{box-sizing:border-box;margin:0;padding:0}html{font-size:15px}
body{font-family:"Source Sans 3",sans-serif;background:var(--off);color:var(--g800);min-height:100vh}
a{color:inherit;text-decoration:none;cursor:pointer}.hidden{display:none!important}
.sidebar{position:fixed;top:0;left:0;width:var(--sw);height:100vh;background:var(--navy);display:flex;flex-direction:column;z-index:100;overflow-y:auto;transition:transform .3s}
.sb-brand{padding:17px 18px 13px;border-bottom:1px solid rgba(201,162,39,.2);display:flex;align-items:center;gap:10px;min-height:var(--hh)}
.sb-icon{width:35px;height:35px;background:var(--gold);border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:16px;color:var(--navy);flex-shrink:0}
.sb-title{font-family:"Playfair Display",serif;font-size:13px;font-weight:700;color:#fff}
.sb-sub{font-size:10px;color:var(--gold);letter-spacing:1.5px;text-transform:uppercase}
.nav-sec{padding:12px 9px 0}.nav-lbl{font-size:9px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:rgba(201,162,39,.55);padding:0 9px 6px}
.nav-item{display:flex;align-items:center;gap:9px;padding:8px 9px;border-radius:6px;color:rgba(255,255,255,.65);font-size:13px;transition:all .15s;margin-bottom:2px;cursor:pointer}
.nav-item:hover{background:rgba(255,255,255,.08);color:#fff}.nav-item.active{background:rgba(201,162,39,.15);color:var(--gold);font-weight:500}
.nav-icon{width:16px;text-align:center;font-size:12px;flex-shrink:0}
.sb-foot{margin-top:auto;padding:12px 9px;border-top:1px solid rgba(255,255,255,.08)}
.user-card{display:flex;align-items:center;gap:8px;padding:8px;border-radius:8px;background:rgba(255,255,255,.06)}
.user-av{width:30px;height:30px;border-radius:50%;background:var(--gold);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:var(--navy);flex-shrink:0}
.user-name{font-size:12px;font-weight:500;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.user-role{font-size:10px;color:var(--gold);text-transform:capitalize}
.main-wrap{margin-left:var(--sw);min-height:100vh;display:flex;flex-direction:column}
.topbar{height:var(--hh);background:#fff;border-bottom:1px solid var(--g200);display:flex;align-items:center;justify-content:space-between;padding:0 24px;position:sticky;top:0;z-index:90;box-shadow:var(--sh)}
.pg-title{font-family:"Playfair Display",serif;font-size:18px;font-weight:600;color:var(--navy)}
.tb-right{display:flex;align-items:center;gap:12px}
.tb-btn{position:relative;width:33px;height:33px;border-radius:7px;background:var(--g100);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--g600);font-size:13px;transition:all .15s}
.tb-btn:hover{background:var(--navy);color:#fff}
.ndot{position:absolute;top:-3px;right:-3px;background:var(--gold);color:var(--navy);font-size:8px;font-weight:700;width:14px;height:14px;border-radius:50%;display:flex;align-items:center;justify-content:center}
.content{flex:1;padding:24px}
.card{background:#fff;border-radius:var(--r);box-shadow:var(--sh);border:1px solid var(--g200);margin-bottom:18px}
.card-hdr{padding:13px 17px;border-bottom:1px solid var(--g100);display:flex;align-items:center;justify-content:space-between}
.card-title{font-family:"Playfair Display",serif;font-size:14.5px;font-weight:600;color:var(--navy)}
.card-body{padding:17px}
.stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(155px,1fr));gap:13px;margin-bottom:20px}
.stat-card{background:#fff;border-radius:var(--r);padding:15px 17px;border:1px solid var(--g200);box-shadow:var(--sh);display:flex;align-items:center;gap:12px;position:relative;overflow:hidden}
/* ── TOP 3 BLOCKERS ── */
.blockers-panel{background:linear-gradient(135deg,#0f172a 0%,#1e1b4b 100%);border-radius:14px;padding:18px 20px;margin-bottom:20px;position:relative;overflow:hidden}
.blockers-panel::before{content:'';position:absolute;top:-40px;right:-40px;width:160px;height:160px;border-radius:50%;background:rgba(244,63,94,.08);pointer-events:none}
.blockers-panel::after{content:'';position:absolute;bottom:-30px;left:20%;width:120px;height:120px;border-radius:50%;background:rgba(99,102,241,.06);pointer-events:none}
.blockers-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
.blockers-hdr-left{display:flex;align-items:center;gap:10px}
.blockers-title{font-size:14px;font-weight:700;color:#fff;letter-spacing:.3px}
.blockers-badge{background:rgba(244,63,94,.2);color:#fda4af;font-size:10px;font-weight:800;padding:2px 9px;border-radius:20px;letter-spacing:.5px;border:1px solid rgba(244,63,94,.3)}
.blockers-sub{font-size:11px;color:rgba(255,255,255,.45);font-weight:500}
.blocker-items{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
@media(max-width:700px){.blocker-items{grid-template-columns:1fr}}
.blocker-item{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:13px 14px;position:relative;cursor:pointer;transition:background .15s,border-color .15s}
.blocker-item:hover{background:rgba(255,255,255,.09);border-color:rgba(255,255,255,.2)}
.blocker-item.sev-critical{border-left:3px solid #f43f5e}
.blocker-item.sev-high{border-left:3px solid #f97316}
.blocker-item.sev-medium{border-left:3px solid #eab308}
.blocker-rank{font-size:9px;font-weight:800;letter-spacing:1px;text-transform:uppercase;margin-bottom:7px;display:flex;align-items:center;gap:6px}
.blocker-rank-num{width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:900;flex-shrink:0}
.blocker-name{font-size:12.5px;font-weight:700;color:#fff;line-height:1.35;margin-bottom:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.blocker-meta{font-size:10.5px;color:rgba(255,255,255,.5);line-height:1.5}
.blocker-days{display:inline-flex;align-items:center;gap:4px;background:rgba(244,63,94,.15);color:#fca5a5;font-size:9.5px;font-weight:800;padding:2px 7px;border-radius:10px;margin-top:7px;border:1px solid rgba(244,63,94,.2)}
.blocker-days.amber{background:rgba(234,179,8,.12);color:#fde047;border-color:rgba(234,179,8,.2)}
.blocker-days.orange{background:rgba(249,115,22,.12);color:#fdba74;border-color:rgba(249,115,22,.2)}
.blocker-type-pill{font-size:9px;font-weight:700;padding:1px 6px;border-radius:8px;display:inline-block;margin-bottom:5px}
.tp-risk{background:rgba(139,92,246,.2);color:#c4b5fd}
.tp-activity{background:rgba(14,165,233,.2);color:#7dd3fc}
.tp-decision{background:rgba(245,158,11,.2);color:#fde68a}
.no-blockers{text-align:center;padding:22px;color:rgba(255,255,255,.4);font-size:13px}
.no-blockers i{font-size:28px;display:block;margin-bottom:8px;opacity:.4}
.escalate-btn{display:inline-flex;align-items:center;gap:4px;font-size:9.5px;font-weight:700;color:rgba(255,255,255,.5);background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);border-radius:6px;padding:3px 8px;margin-top:8px;cursor:pointer;transition:background .15s,color .15s}
.escalate-btn:hover{background:rgba(244,63,94,.2);color:#fda4af;border-color:rgba(244,63,94,.3)}
.stat-card::before{content:"";position:absolute;top:0;left:0;width:4px;height:100%;background:var(--sc,var(--gold))}
.stat-icon{width:38px;height:38px;border-radius:9px;background:var(--scb,var(--gp));display:flex;align-items:center;justify-content:center;color:var(--sc,var(--gold));font-size:16px;flex-shrink:0}
.stat-val{font-size:23px;font-weight:700;color:var(--navy);line-height:1}.stat-lbl{font-size:10.5px;color:var(--g400);margin-top:3px}
.badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:20px;font-size:11px;font-weight:600}
.b-success{background:var(--sbg);color:var(--success)}.b-warn{background:var(--wbg);color:var(--warn)}
.b-danger{background:var(--dbg);color:var(--danger)}.b-info{background:var(--ibg);color:var(--info)}
.b-gray{background:var(--g100);color:var(--g600)}.b-navy{background:var(--navy);color:#fff}.b-gold{background:var(--gp);color:var(--warn)}
.btn{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:6px;border:none;cursor:pointer;font-size:13px;font-weight:500;font-family:inherit;transition:all .15s;white-space:nowrap}
.btn-primary{background:var(--navy);color:#fff}.btn-primary:hover{background:var(--nm)}
.btn-gold{background:var(--gold);color:var(--navy)}.btn-gold:hover{background:var(--gl)}
.btn-outline{background:transparent;border:1px solid var(--g200);color:var(--g600)}.btn-outline:hover{border-color:var(--navy);color:var(--navy)}
.btn-danger{background:var(--dbg);color:var(--danger);border:1px solid #fca5a5}.btn-danger:hover{background:var(--danger);color:#fff}
.btn-success{background:var(--sbg);color:var(--success);border:1px solid #86efac}
.btn-sm{padding:5px 10px;font-size:12px}.btn-xs{padding:3px 7px;font-size:11px}
.tbl-wrap{overflow-x:auto}table{width:100%;border-collapse:collapse}
th{background:var(--navy);color:#fff;padding:9px 12px;font-size:11px;font-weight:600;letter-spacing:.8px;text-transform:uppercase;text-align:left;white-space:nowrap}
td{padding:9px 12px;border-bottom:1px solid var(--g100);font-size:13px;vertical-align:middle}
tr:last-child td{border-bottom:none}tr:hover td{background:var(--g50)}
.fg{margin-bottom:13px}
.fg label{display:block;font-size:11px;font-weight:600;color:var(--g600);margin-bottom:4px;letter-spacing:.3px;text-transform:uppercase}
.fg input,.fg select,.fg textarea{width:100%;padding:8px 10px;border:1px solid var(--g200);border-radius:6px;font-family:inherit;font-size:13px;color:var(--g800);background:#fff;transition:border-color .15s}
.fg input:focus,.fg select:focus,.fg textarea:focus{outline:none;border-color:var(--nl);box-shadow:0 0 0 3px rgba(46,95,138,.1)}
.fg textarea{resize:vertical;min-height:70px}.fg-2{display:grid;grid-template-columns:1fr 1fr;gap:13px}.fg-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:13px}
.prog-wrap{background:var(--g200);border-radius:20px;height:7px;overflow:hidden}
.prog-bar{height:100%;background:linear-gradient(90deg,var(--nm),var(--gold));border-radius:20px;transition:width .5s}
.prog-bar.ps{background:linear-gradient(90deg,#15803d,#4ade80)}.prog-bar.pd{background:linear-gradient(90deg,#dc2626,#f87171)}
.alert{padding:10px 13px;border-radius:6px;font-size:13px;display:flex;align-items:center;gap:8px;margin-bottom:13px;animation:fadeIn .3s}
@keyframes fadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
.alert-success{background:var(--sbg);color:var(--success);border-left:4px solid var(--success)}
.alert-danger{background:var(--dbg);color:var(--danger);border-left:4px solid var(--danger)}
.overlay{position:fixed;inset:0;background:rgba(13,33,55,.55);z-index:500;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s}
.overlay.open{opacity:1;pointer-events:all}
.modal{background:#fff;border-radius:12px;width:100%;max-width:580px;max-height:92vh;overflow-y:auto;box-shadow:var(--shl);transform:translateY(16px);transition:transform .2s}
.overlay.open .modal{transform:none}
.modal-hdr{padding:15px 20px;border-bottom:1px solid var(--g100);display:flex;align-items:center;justify-content:space-between;background:var(--navy);border-radius:12px 12px 0 0;position:sticky;top:0;z-index:10}
.modal-title{font-family:"Playfair Display",serif;font-size:15px;color:#fff}
.modal-close{background:none;border:none;color:rgba(255,255,255,.6);font-size:17px;cursor:pointer}.modal-close:hover{color:#fff}
.modal-body{padding:20px}.modal-foot{padding:12px 20px;border-top:1px solid var(--g100);display:flex;justify-content:flex-end;gap:8px;background:var(--g50);border-radius:0 0 12px 12px}
.nd{position:absolute;top:40px;right:0;width:310px;background:#fff;border-radius:10px;box-shadow:var(--shl);border:1px solid var(--g200);z-index:300;display:none;max-height:400px;overflow-y:auto}
.nd.open{display:block}.nd-hdr{padding:10px 14px;border-bottom:1px solid var(--g100);display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;background:#fff}
.nd-item{padding:10px 14px;border-bottom:1px solid var(--g100);font-size:12.5px;cursor:pointer;transition:background .15s}
.nd-item:hover{background:var(--g50)}.nd-item.unread{background:rgba(201,162,39,.06)}
.login-screen{position:fixed;inset:0;background:var(--navy);display:flex;align-items:center;justify-content:center;z-index:999}
.login-box{background:#fff;border-radius:16px;overflow:hidden;width:380px;box-shadow:var(--shl)}
.login-hdr{background:var(--navy);padding:28px 32px;text-align:center;border-bottom:3px solid var(--gold)}
.login-logo{width:52px;height:52px;background:var(--gold);border-radius:12px;margin:0 auto 12px;display:flex;align-items:center;justify-content:center;font-size:22px;color:var(--navy)}
.login-body{padding:26px 32px}
.login-body label{display:block;font-size:11px;font-weight:700;color:var(--g600);margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px}
.login-body input{width:100%;padding:9px 12px;border:1.5px solid var(--g200);border-radius:7px;font-size:13.5px;font-family:inherit;margin-bottom:13px}
.login-body input:focus{outline:none;border-color:var(--nl)}
.login-btn{width:100%;padding:11px;background:var(--navy);color:#fff;border:none;border-radius:7px;font-family:inherit;font-size:14px;font-weight:600;cursor:pointer}
.login-btn:hover{background:var(--nm)}
.login-err{background:var(--dbg);color:var(--danger);padding:8px 12px;border-radius:6px;font-size:12.5px;margin-bottom:12px;border-left:3px solid var(--danger)}
.phase-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:15px}
.phase-card{background:#fff;border-radius:10px;padding:16px;border:1px solid var(--g200);box-shadow:var(--sh);border-top:4px solid var(--pc,var(--gold));transition:box-shadow .2s,transform .2s;cursor:pointer}
.phase-card:hover{box-shadow:var(--shm);transform:translateY(-2px)}
.phase-card.active-phase{border:2px solid var(--pc,var(--gold));border-top:4px solid var(--pc,var(--gold));box-shadow:0 0 0 3px rgba(201,162,39,.18),var(--shm);background:linear-gradient(160deg,#fffbef 0%,#fff 60%);position:relative}
.phase-card.active-phase::after{content:"IN PROGRESS";position:absolute;top:10px;right:10px;font-size:8.5px;font-weight:800;letter-spacing:1.2px;background:var(--pc,var(--gold));color:#fff;padding:2px 7px;border-radius:20px}
.upcoming-list{list-style:none;padding:0;margin:0}
.stat-tiles{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-top:16px}
.stat-tile{border-radius:14px;padding:14px 16px;display:flex;align-items:center;gap:13px;cursor:default;transition:transform .18s,box-shadow .18s;position:relative;overflow:hidden;border:1px solid transparent}
.stat-tile:hover{transform:translateY(-3px);box-shadow:0 8px 24px rgba(0,0,0,.13)}
.stat-tile-icon{width:42px;height:42px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:17px;flex-shrink:0;box-shadow:0 4px 10px rgba(0,0,0,.12);color:#fff}
.stat-tile-val{font-size:26px;font-weight:900;line-height:1;font-family:'Playfair Display',serif;letter-spacing:-1px;color:#0f172a}
.stat-tile-lbl{font-size:10px;font-weight:800;letter-spacing:.8px;text-transform:uppercase;margin-top:3px;color:#475569}
.stat-tile-pct{font-size:10px;font-weight:700;margin-top:2px;opacity:.75}
.chart-centre-wrap{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none}
.chart-centre-val{font-size:30px;font-weight:900;color:#0f172a;font-family:'Playfair Display',serif;line-height:1;transition:color .2s}
.chart-centre-lbl{font-size:9.5px;font-weight:800;color:#64748b;letter-spacing:1.2px;text-transform:uppercase;margin-top:3px}
.upcoming-item{display:flex;align-items:center;gap:12px;padding:9px 0;border-bottom:1px solid var(--g100)}
.upcoming-item:last-child{border-bottom:none}
.upcoming-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
.upcoming-days{font-size:10px;font-weight:700;min-width:52px;text-align:right;flex-shrink:0}
.pill{display:inline-flex;align-items:center;gap:4px;padding:3px 9px;background:var(--g50);border:1px solid var(--g200);border-radius:6px;font-size:11.5px;color:var(--g600);cursor:pointer;transition:all .15s}
.pill:hover{border-color:var(--navy);color:var(--navy)}
.bc{display:flex;align-items:center;gap:5px;font-size:12px;color:var(--g400);margin-bottom:14px}
.bc .lnk{color:var(--gold);cursor:pointer}.bc .lnk:hover{text-decoration:underline}
.divider{height:1px;background:var(--g200);margin:13px 0}
.flex{display:flex}.ic{align-items:center}.jb{justify-content:space-between}
.gap-2{gap:8px}.gap-3{gap:12px}.mt-2{margin-top:8px}.mt-3{margin-top:12px}.mt-4{margin-top:16px}
.mb-3{margin-bottom:12px}.mb-4{margin-bottom:16px}.fw6{font-weight:600}.fs12{font-size:12px}.fs13{font-size:13px}
.muted{color:var(--g400)}.tgold{color:var(--gold)}.tnavy{color:var(--navy)}
.empty{text-align:center;padding:36px 16px;color:var(--g400)}.empty i{font-size:30px;margin-bottom:10px;opacity:.35;display:block}
/* ── PMT / PSC COMMITTEES ── */
.committee-banner{display:flex;gap:12px;margin-bottom:18px;flex-wrap:wrap}
.committee-card{flex:1;min-width:220px;border-radius:14px;padding:16px 18px;position:relative;overflow:hidden;border:1px solid transparent}
.committee-card::before{content:'';position:absolute;top:-30px;right:-30px;width:100px;height:100px;border-radius:50%;opacity:.12;pointer-events:none}
.committee-card.pmt{background:linear-gradient(135deg,#1e3a5f,#1e4976);border-color:#2563eb44}
.committee-card.pmt::before{background:#60a5fa}
.committee-card.psc{background:linear-gradient(135deg,#3b1f6e,#4c1d95);border-color:#7c3aed44}
.committee-card.psc::before{background:#a78bfa}
.committee-hdr{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.committee-icon{width:36px;height:36px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:15px;flex-shrink:0}
.committee-card.pmt .committee-icon{background:rgba(96,165,250,.2);color:#60a5fa}
.committee-card.psc .committee-icon{background:rgba(167,139,250,.2);color:#a78bfa}
.committee-name{font-size:13px;font-weight:800;color:#fff;letter-spacing:.3px}
.committee-role{font-size:10px;color:rgba(255,255,255,.5);margin-top:1px;font-weight:500}
.committee-stats{display:flex;gap:8px;flex-wrap:wrap}
.committee-stat{background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:7px 10px;text-align:center;flex:1;min-width:60px}
.committee-stat-val{font-size:18px;font-weight:900;color:#fff;font-family:'Playfair Display',serif;line-height:1}
.committee-stat-lbl{font-size:9px;color:rgba(255,255,255,.5);margin-top:2px;font-weight:600;letter-spacing:.4px;text-transform:uppercase}
.committee-members{display:flex;gap:4px;flex-wrap:wrap;margin-top:10px}
.cm-avatar{width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:800;color:#fff;border:2px solid rgba(255,255,255,.15);cursor:pointer;transition:transform .15s}
.cm-avatar:hover{transform:scale(1.15);z-index:2}
.cm-more{background:rgba(255,255,255,.1);color:rgba(255,255,255,.6);font-size:9px}
/* Approval badges on milestone/deliverable cards */
.approval-row{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}
.approval-badge{display:inline-flex;align-items:center;gap:4px;font-size:9.5px;font-weight:700;padding:3px 8px;border-radius:6px;letter-spacing:.3px}
.ab-pmt-endorsed{background:rgba(37,99,235,.12);color:#3b82f6;border:1px solid rgba(37,99,235,.25)}
.ab-pmt-escalated{background:rgba(239,68,68,.1);color:#dc2626;border:1px solid rgba(239,68,68,.25)}
.ab-pmt-pending{background:rgba(234,179,8,.1);color:#ca8a04;border:1px solid rgba(234,179,8,.25)}
.ab-pmt-returned{background:rgba(249,115,22,.1);color:#ea580c;border:1px solid rgba(249,115,22,.25)}
.ab-psc-approved{background:rgba(124,58,237,.12);color:#7c3aed;border:1px solid rgba(124,58,237,.25)}
.ab-psc-pending{background:rgba(249,115,22,.1);color:#ea580c;border:1px solid rgba(249,115,22,.25)}
.ab-psc-rejected{background:rgba(239,68,68,.1);color:#dc2626;border:1px solid rgba(239,68,68,.25)}
/* Committee page */
.committee-section{background:#fff;border-radius:12px;border:1px solid var(--g200);box-shadow:var(--sh);margin-bottom:18px;overflow:hidden}
.committee-section-hdr{padding:14px 18px;border-bottom:1px solid var(--g100);display:flex;align-items:center;justify-content:space-between}
.committee-section-title{font-size:14px;font-weight:700;display:flex;align-items:center;gap:8px}
.approval-table{width:100%;border-collapse:collapse}
.approval-table th{background:var(--g50);padding:9px 14px;font-size:10px;font-weight:700;text-align:left;color:var(--g500);letter-spacing:.8px;text-transform:uppercase;border-bottom:1px solid var(--g200)}
.approval-table td{padding:10px 14px;font-size:12.5px;border-bottom:1px solid var(--g100);vertical-align:middle}
.approval-table tr:last-child td{border-bottom:none}
.approval-table tr:hover td{background:var(--g50)}
.status-pill{display:inline-flex;align-items:center;gap:4px;padding:3px 9px;border-radius:20px;font-size:10px;font-weight:700}
.sp-approved{background:#dcfce7;color:#15803d}
.sp-pending{background:#fef9c3;color:#854d0e}
.sp-rejected{background:#fee2e2;color:#991b1b}
.sp-not-required{background:#f1f5f9;color:#64748b}
.workflow-line{display:flex;align-items:center;gap:0;margin:16px 0;overflow-x:auto;padding:0 18px}
.wf-step{display:flex;flex-direction:column;align-items:center;min-width:90px}
.wf-dot{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;border:2px solid}
.wf-dot.done{background:#dcfce7;border-color:#16a34a;color:#16a34a}
.wf-dot.active{background:#dbeafe;border-color:#2563eb;color:#2563eb;animation:pulse-green 2s infinite}
.wf-dot.pending{background:#f8fafc;border-color:#cbd5e1;color:#94a3b8}
.wf-lbl{font-size:9.5px;font-weight:700;color:var(--g500);margin-top:5px;text-align:center;max-width:80px;line-height:1.3}
.wf-arrow{flex:1;height:2px;background:linear-gradient(90deg,#e2e8f0,#e2e8f0);min-width:20px;margin-bottom:18px}
.wf-arrow.done{background:linear-gradient(90deg,#16a34a,#16a34a)}
/* == JSONBIN SYNC == */
.jb-status-bar{display:flex;align-items:center;gap:6px;font-size:11px;padding:5px 10px;border-radius:6px;cursor:pointer;color:rgba(255,255,255,.7);transition:background .15s;margin-top:2px}
.jb-status-bar:hover{background:rgba(255,255,255,.1)}
.jb-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;transition:background .3s}
.jb-dot.live{background:#22c55e;box-shadow:0 0 0 3px rgba(34,197,94,.2);animation:pulse-green 2s infinite}
.jb-dot.syncing{background:#f59e0b;animation:pulse-green 2s infinite}
.jb-dot.off{background:#6b7280}
.jb-dot.error{background:#ef4444}
/* == SYNC STATUS == */
.sync-dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:5px;flex-shrink:0}
.sync-dot.live{background:#22c55e;box-shadow:0 0 0 2px rgba(34,197,94,.25);animation:pulse-green 2s infinite}
.sync-dot.local{background:#f59e0b}
.sync-dot.error{background:#ef4444}
@keyframes pulse-green{0%,100%{box-shadow:0 0 0 2px rgba(34,197,94,.25)}50%{box-shadow:0 0 0 5px rgba(34,197,94,.1)}}
.sync-bar{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--g400);padding:3px 8px;background:rgba(255,255,255,.06);border-radius:6px;cursor:pointer}
.sync-bar:hover{background:rgba(255,255,255,.1)}
/* == GANTT ENHANCED == */
/* ═══════════════════════════════════════
GANTT CHART — REVAMPED PROFESSIONAL
═══════════════════════════════════════ */
:root{
--g-ph1:#6366f1; /* indigo */
--g-ph2:#0ea5e9; /* sky blue */
--g-ph3:#10b981; /* emerald */
--g-ph4:#f59e0b; /* amber */
--g-ph5:#ec4899; /* pink */
--g-ph6:#8b5cf6; /* violet */
--g-done:#10b981;
--g-prog:#6366f1;
--g-delay:#f43f5e;
--g-none:#94a3b8;
--g-today:#f43f5e;
}
.gantt-wrap{overflow-x:auto;padding:0;position:relative;user-select:none;border-radius:10px;box-shadow:0 4px 24px rgba(0,0,0,.10)}
.gantt-header{display:flex;background:linear-gradient(135deg,#0f172a 0%,#1e293b 100%);border-radius:10px 10px 0 0;position:sticky;top:0;z-index:10}
.gantt-label-col{width:220px;flex-shrink:0;padding:12px 16px;font-size:9.5px;font-weight:800;color:rgba(255,255,255,.5);letter-spacing:2px;text-transform:uppercase;border-right:1px solid rgba(255,255,255,.08)}
.gantt-months{flex:1;display:flex;position:relative}
.gantt-month{font-size:10px;color:rgba(255,255,255,.7);padding:12px 8px;font-weight:700;border-left:1px solid rgba(255,255,255,.08);letter-spacing:.3px;white-space:nowrap;overflow:hidden;transition:background .2s}
.gantt-month-active{background:rgba(99,102,241,.25)!important;color:#fff!important;position:relative}
.gantt-month-active::after{content:'NOW';position:absolute;bottom:3px;right:5px;font-size:7px;font-weight:900;color:#a5b4fc;letter-spacing:1px}
/* Zebra rows */
.gantt-body{min-width:600px;background:#fff}
.gantt-row{display:flex;align-items:center;border-bottom:1px solid #f1f5f9;min-height:46px;transition:background .15s;position:relative}
.gantt-row:nth-child(even){background:#fafbff}
.gantt-row:nth-child(odd){background:#fff}
.gantt-row:hover{background:#f0f4ff!important}
.gantt-row:last-child{border-bottom:none}
/* Label column */
.gantt-row-label{width:220px;flex-shrink:0;padding:8px 14px;border-right:2px solid #f1f5f9;display:flex;flex-direction:column;gap:3px}
.gantt-row-label .mil-name{font-weight:700;color:#0f172a;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:190px;line-height:1.3}
.gantt-row-label .ph-tag{font-size:9.5px;font-weight:800;letter-spacing:.8px;padding:1px 6px;border-radius:20px;display:inline-block;width:fit-content}
/* Track & bars */
.gantt-track{flex:1;position:relative;height:46px}
.gantt-bar{position:absolute;top:10px;height:26px;border-radius:8px;display:flex;align-items:center;padding:0 10px;cursor:pointer;transition:transform .15s,box-shadow .15s;overflow:hidden}
.gantt-bar:hover{transform:scaleY(1.08) translateY(-1px);box-shadow:0 6px 20px rgba(0,0,0,.25)!important;z-index:6}
.gantt-bar-fill{position:absolute;left:0;top:0;height:100%;background:rgba(255,255,255,.22);border-radius:8px 0 0 8px;transition:width .7s cubic-bezier(.4,0,.2,1)}
.gantt-bar-pct{font-size:9px;color:rgba(255,255,255,.95);font-weight:900;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;position:relative;z-index:1;text-shadow:0 1px 2px rgba(0,0,0,.3)}
.gantt-bar-shine{position:absolute;top:0;left:0;right:0;height:50%;background:linear-gradient(180deg,rgba(255,255,255,.2),transparent);border-radius:8px 8px 0 0;pointer-events:none}
/* Active row */
.gantt-active-row{background:linear-gradient(90deg,#f0f4ff,#fafbff)!important}
.gantt-active-row .gantt-row-label{border-left:3px solid #6366f1}
/* TODAY elements */
.gantt-now-pointer{position:absolute;top:-36px;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;z-index:20;pointer-events:none;filter:drop-shadow(0 2px 4px rgba(244,63,94,.4))}
.gantt-now-label{background:linear-gradient(135deg,#f43f5e,#e11d48);color:#fff;font-size:8px;font-weight:900;padding:3px 8px;border-radius:5px;letter-spacing:.8px;white-space:nowrap}
.gantt-now-arrow{width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid #e11d48}
.gantt-now-line{position:absolute;top:0;width:2px;height:100%;z-index:15;pointer-events:none;background:linear-gradient(180deg,#f43f5e,rgba(244,63,94,.15));box-shadow:0 0 8px rgba(244,63,94,.35)}
.gantt-pulse-dot{width:12px;height:12px;border-radius:50%;background:#f43f5e;border:2px solid #fff;position:absolute;transform:translate(-50%,-50%);top:50%;z-index:20;animation:gantt-pulse 1.8s ease-out infinite}
@keyframes gantt-pulse{0%{box-shadow:0 0 0 0 rgba(244,63,94,.6)}70%{box-shadow:0 0 0 10px rgba(244,63,94,0)}100%{box-shadow:0 0 0 0 rgba(244,63,94,0)}}
/* Tooltip */
.gantt-tooltip{position:fixed;background:linear-gradient(135deg,#0f172a,#1e293b);color:#fff;padding:12px 16px;border-radius:10px;font-size:12px;z-index:9999;pointer-events:none;box-shadow:0 8px 32px rgba(0,0,0,.35);border-left:3px solid #6366f1;max-width:280px;display:none;line-height:1.6}
/* Legend */
.gantt-legend{display:flex;flex-wrap:wrap;gap:12px;padding:12px 16px;border-top:2px solid #f1f5f9;background:linear-gradient(135deg,#f8fafc,#f1f5f9);border-radius:0 0 10px 10px;align-items:center}
.gantt-legend-item{display:flex;align-items:center;gap:6px;font-size:11px;color:#64748b;font-weight:600}
.gantt-legend-dot{width:14px;height:14px;border-radius:4px;flex-shrink:0}
/* Phase colour pills */
.ph-pill-1{background:rgba(99,102,241,.12);color:#6366f1}
.ph-pill-2{background:rgba(14,165,233,.12);color:#0ea5e9}
.ph-pill-3{background:rgba(16,185,129,.12);color:#10b981}
.ph-pill-4{background:rgba(245,158,11,.12);color:#d97706}
.ph-pill-5{background:rgba(236,72,153,.12);color:#ec4899}
.ph-pill-6{background:rgba(139,92,246,.12);color:#8b5cf6}
/* Month grid lines in track */
.gantt-grid-line{position:absolute;top:0;width:1px;height:100%;background:#f1f5f9;pointer-events:none;z-index:1}
/* ── EXECUTIVE SUMMARY ── */
.exec-hero{background:linear-gradient(135deg,#0f172a 0%,#1e2d5a 60%,#1e1b4b 100%);border-radius:16px;padding:24px 28px;margin-bottom:20px;position:relative;overflow:hidden;color:#fff}
.exec-hero::before{content:'';position:absolute;top:-60px;right:-60px;width:220px;height:220px;border-radius:50%;background:rgba(99,102,241,.1);pointer-events:none}
.exec-hero::after{content:'';position:absolute;bottom:-40px;left:10%;width:160px;height:160px;border-radius:50%;background:rgba(201,162,39,.06);pointer-events:none}
.exec-hero-top{display:flex;align-items:flex-start;justify-content:space-between;flex-wrap:wrap;gap:16px;margin-bottom:20px}
.exec-hero-title{font-size:11px;color:rgba(255,255,255,.5);letter-spacing:1.5px;text-transform:uppercase;font-weight:700;margin-bottom:4px}
.exec-hero-name{font-family:'Playfair Display',serif;font-size:22px;font-weight:700;color:#fff;line-height:1.2}
.exec-hero-sub{font-size:12px;color:rgba(255,255,255,.5);margin-top:4px}
.health-ring-wrap{display:flex;flex-direction:column;align-items:center;gap:6px}
.health-score-big{font-family:'Playfair Display',serif;font-size:48px;font-weight:900;line-height:1;letter-spacing:-2px}
.health-score-lbl{font-size:10px;font-weight:800;letter-spacing:1.5px;text-transform:uppercase;opacity:.6}
.health-trend{font-size:11px;font-weight:700;margin-top:2px}
.exec-kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:4px}
@media(max-width:600px){.exec-kpis{grid-template-columns:repeat(2,1fr)}}
.exec-kpi{background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:12px 14px;text-align:center}
.exec-kpi-val{font-size:24px;font-weight:900;font-family:'Playfair Display',serif;color:#fff;line-height:1}
.exec-kpi-lbl{font-size:9.5px;color:rgba(255,255,255,.5);font-weight:700;letter-spacing:.5px;text-transform:uppercase;margin-top:4px}
.rag-dot{width:10px;height:10px;border-radius:50%;display:inline-block;margin-right:4px}
.rag-green{background:#22c55e}
.rag-amber{background:#f59e0b}
.rag-red{background:#ef4444}
/* Narrative box */
.narrative-box{background:#fff;border:1px solid var(--g200);border-radius:12px;padding:16px 18px;margin-bottom:16px}
.narrative-box .nb-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
.narrative-box .nb-title{font-size:13px;font-weight:700;color:var(--navy);display:flex;align-items:center;gap:7px}
.narrative-text{font-size:13px;color:var(--g600);line-height:1.7;white-space:pre-wrap;min-height:60px}
/* Decisions Required */
.dec-required-panel{background:#fff;border:1px solid var(--g200);border-radius:12px;overflow:hidden;margin-bottom:16px}
.dec-required-hdr{padding:14px 18px;background:linear-gradient(135deg,#1e3a5f,#1e2d5a);display:flex;align-items:center;justify-content:space-between}
.dec-required-title{font-size:14px;font-weight:800;color:#fff;display:flex;align-items:center;gap:8px}
.dec-item{display:flex;align-items:flex-start;gap:14px;padding:13px 18px;border-bottom:1px solid var(--g100)}
.dec-item:last-child{border-bottom:none}
.dec-item:hover{background:var(--g50)}
.dec-urgency{width:42px;height:42px;border-radius:10px;display:flex;flex-direction:column;align-items:center;justify-content:center;flex-shrink:0;font-size:14px;font-weight:900;line-height:1}
.dec-urgency-lbl{font-size:7px;font-weight:800;letter-spacing:.5px;text-transform:uppercase;margin-top:2px}
.dec-body{flex:1;min-width:0}
.dec-title-text{font-size:13px;font-weight:700;color:var(--navy);margin-bottom:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dec-meta{font-size:11px;color:var(--g400);line-height:1.5}
.dec-consequence{font-size:11px;color:#b91c1c;font-weight:600;margin-top:4px;display:flex;align-items:center;gap:4px}
.dec-action-btn{flex-shrink:0;align-self:center}
/* Meetings tab */
.meeting-card{background:#fff;border:1px solid var(--g200);border-radius:12px;margin-bottom:14px;overflow:hidden}
.meeting-card-hdr{padding:14px 18px;display:flex;align-items:center;justify-content:space-between;cursor:pointer;transition:background .15s}
.meeting-card-hdr:hover{background:var(--g50)}
.meeting-type-badge{font-size:10px;font-weight:800;padding:3px 10px;border-radius:20px;letter-spacing:.5px}
.mtb-pmt{background:#dbeafe;color:#1d4ed8}
.mtb-psc{background:#ede9fe;color:#6d28d9}
.meeting-body{padding:0 18px 16px;display:none}
.meeting-body.open{display:block}
.agenda-item{display:flex;align-items:flex-start;gap:10px;padding:8px 0;border-bottom:1px solid var(--g100)}
.agenda-item:last-child{border-bottom:none}
.agenda-num{width:22px;height:22px;border-radius:50%;background:var(--navy);color:#fff;font-size:10px;font-weight:800;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px}
.agenda-text{font-size:12.5px;color:var(--g700);flex:1}
.agenda-status{font-size:10px;font-weight:700;padding:2px 7px;border-radius:8px;margin-left:auto;white-space:nowrap;flex-shrink:0}
.as-agreed{background:#dcfce7;color:#15803d}
.as-noted{background:#dbeafe;color:#1d4ed8}
.as-deferred{background:#fef9c3;color:#854d0e}
.as-pending{background:#fee2e2;color:#991b1b}
.meeting-decision-box{background:linear-gradient(135deg,#f0fdf4,#dcfce7);border:1px solid #bbf7d0;border-radius:8px;padding:12px 14px;margin-top:12px}
.meeting-decision-box h5{font-size:11px;font-weight:800;color:#15803d;letter-spacing:.5px;text-transform:uppercase;margin-bottom:5px}
.meeting-decision-box p{font-size:12.5px;color:#166534;line-height:1.6}
/* ── SCHEDULE PERFORMANCE ── */
.spi-wrap{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px}
@media(max-width:600px){.spi-wrap{grid-template-columns:1fr}}
.spi-card{background:#fff;border:1px solid var(--g200);border-radius:14px;padding:18px 20px;position:relative;overflow:hidden}
.spi-card::before{content:'';position:absolute;top:-20px;right:-20px;width:80px;height:80px;border-radius:50%;opacity:.08;pointer-events:none}
.spi-card.blue::before{background:#6366f1}
.spi-card.green::before{background:#10b981}
.spi-card.amber::before{background:#f59e0b}
.spi-card.red::before{background:#f43f5e}
.spi-label{font-size:10px;font-weight:800;letter-spacing:1px;text-transform:uppercase;color:var(--g400);margin-bottom:8px}
.spi-value{font-size:36px;font-weight:900;font-family:'Playfair Display',serif;line-height:1;letter-spacing:-1px}
.spi-sub{font-size:12px;color:var(--g500);margin-top:5px;line-height:1.5}
.spi-bar-wrap{margin-top:12px}
.spi-bar-labels{display:flex;justify-content:space-between;font-size:10px;color:var(--g400);font-weight:600;margin-bottom:4px}
.spi-bar-track{height:8px;background:var(--g100);border-radius:4px;position:relative;overflow:visible}
.spi-bar-fill{height:100%;border-radius:4px;transition:width .8s cubic-bezier(.4,0,.2,1)}
.spi-bar-needle{position:absolute;top:-4px;width:3px;height:16px;border-radius:2px;transform:translateX(-50%);z-index:2}
.proj-timeline{background:#fff;border:1px solid var(--g200);border-radius:14px;padding:18px 20px;margin-bottom:16px}
.proj-tl-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;flex-wrap:wrap;gap:8px}
.proj-tl-title{font-size:13px;font-weight:700;color:var(--navy);display:flex;align-items:center;gap:7px}
.proj-tl-track{position:relative;height:12px;background:var(--g100);border-radius:6px;margin:10px 0 6px}
.proj-tl-elapsed{height:100%;border-radius:6px;background:linear-gradient(90deg,#6366f1,#4f46e5);transition:width .8s ease}
.proj-tl-done{position:absolute;top:0;height:100%;border-radius:6px;background:linear-gradient(90deg,#10b981,#059669);opacity:.7}
.proj-tl-needle{position:absolute;top:-6px;width:4px;height:24px;background:#f43f5e;border-radius:2px;transform:translateX(-50%);box-shadow:0 0 6px rgba(244,63,94,.4)}
.proj-tl-needle::before{content:'TODAY';position:absolute;top:-16px;left:50%;transform:translateX(-50%);font-size:7px;font-weight:900;color:#f43f5e;white-space:nowrap;letter-spacing:.5px}
.proj-badges{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
.proj-badge{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:700;padding:4px 10px;border-radius:8px}
.pb-blue{background:#eef2ff;color:#4f46e5}
.pb-green{background:#f0fdf4;color:#15803d}
.pb-amber{background:#fffbeb;color:#b45309}
.pb-red{background:#fff1f2;color:#be123c}
.variance-box{display:flex;align-items:center;gap:10px;padding:10px 14px;border-radius:10px;margin-top:10px;font-size:12.5px;font-weight:600}
.vb-green{background:#f0fdf4;border:1px solid #bbf7d0;color:#15803d}
.vb-amber{background:#fffbeb;border:1px solid #fde68a;color:#b45309}
.vb-red{background:#fff1f2;border:1px solid #fecdd3;color:#be123c}
/* ── STAKEHOLDER TRACKER ── */
.sh-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;margin-bottom:16px}
.sh-card{background:#fff;border:1px solid var(--g200);border-radius:12px;overflow:hidden;transition:box-shadow .15s,transform .15s}
.sh-card:hover{box-shadow:var(--shm);transform:translateY(-2px)}
.sh-card-top{padding:14px 16px;display:flex;align-items:flex-start;gap:12px}
.sh-avatar{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:17px;font-weight:900;color:#fff;flex-shrink:0}
.sh-name{font-size:13px;font-weight:700;color:var(--navy);line-height:1.2}
.sh-org{font-size:11px;color:var(--g400);margin-top:2px}
.sh-type{font-size:9.5px;font-weight:800;padding:2px 7px;border-radius:10px;display:inline-block;margin-top:4px;letter-spacing:.4px}
.st-internal{background:#dbeafe;color:#1d4ed8}
.st-regulatory{background:#fce7f3;color:#9d174d}
.st-vendor{background:#dcfce7;color:#15803d}
.st-board{background:#ede9fe;color:#6d28d9}
.st-external{background:#fef9c3;color:#854d0e}
.sh-card-bottom{padding:8px 16px 12px;border-top:1px solid var(--g100)}
.sh-engagement-bar{height:5px;background:var(--g100);border-radius:3px;margin:6px 0 4px}
.sh-engagement-fill{height:100%;border-radius:3px;transition:width .6s ease}
.sh-meta{display:flex;justify-content:space-between;align-items:center;font-size:10.5px;color:var(--g400)}
.sh-status-dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:4px}
.eng-high{background:#22c55e}
.eng-medium{background:#f59e0b}
.eng-low{background:#ef4444}
.eng-inactive{background:#9ca3af}
.sh-last{font-size:10px;color:var(--g300)}
.sh-filter-bar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px;align-items:center}
.sh-filter-btn{font-size:11px;font-weight:700;padding:4px 12px;border-radius:20px;border:1px solid var(--g200);background:#fff;color:var(--g500);cursor:pointer;transition:all .15s}
.sh-filter-btn.active{background:var(--navy);color:#fff;border-color:var(--navy)}
.sh-summary-row{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px}
.sh-sum-tile{flex:1;min-width:80px;background:#fff;border:1px solid var(--g200);border-radius:10px;padding:10px 12px;text-align:center}
.sh-sum-val{font-size:22px;font-weight:900;font-family:'Playfair Display',serif;color:var(--navy)}
.sh-sum-lbl{font-size:9.5px;font-weight:700;color:var(--g400);letter-spacing:.4px;text-transform:uppercase;margin-top:2px}
/* == EMAIL TOGGLE == */
.email-toggle{display:flex;align-items:center;gap:10px}
.toggle-switch{position:relative;width:38px;height:20px;flex-shrink:0}
.toggle-switch input{opacity:0;width:0;height:0;position:absolute}
.toggle-slider{position:absolute;inset:0;background:var(--g200);border-radius:20px;cursor:pointer;transition:.3s}
.toggle-slider::before{content:'';position:absolute;height:14px;width:14px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.3s;box-shadow:0 1px 3px rgba(0,0,0,.2)}
.toggle-switch input:checked+.toggle-slider{background:var(--gold)}
.toggle-switch input:checked+.toggle-slider::before{transform:translateX(18px)}
/* == SETUP BANNER == */
.setup-banner{background:linear-gradient(135deg,#1e3a5f,#2563eb);color:#fff;padding:12px 18px;border-radius:8px;margin-bottom:16px;font-size:13px;display:flex;align-items:flex-start;gap:12px}
.setup-banner i{font-size:20px;flex-shrink:0;margin-top:2px;color:#93c5fd}
</style>
<style>
.od-bar{display:flex;align-items:center;gap:6px;font-size:11px;padding:4px 10px;border-radius:6px;cursor:pointer;transition:background .15s;color:rgba(255,255,255,.7);margin-top:4px}
.od-bar:hover{background:rgba(255,255,255,.1)}
.od-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;transition:background .3s}
.od-dot.connected{background:#22c55e;box-shadow:0 0 0 3px rgba(34,197,94,.2);animation:pulse-green 2s infinite}
.od-dot.syncing{background:#f59e0b;animation:pulse-amber 1s infinite}
.od-dot.disconnected{background:#6b7280}
.od-dot.error{background:#ef4444}
@keyframes pulse-amber{0%,100%{box-shadow:0 0 0 2px rgba(245,158,11,.2)}50%{box-shadow:0 0 0 5px rgba(245,158,11,.05)}}
.od-badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:700;padding:2px 8px;border-radius:20px;letter-spacing:.4px}
.od-badge.on{background:#dcfce7;color:#15803d}
.od-badge.off{background:#f3f4f6;color:#6b7280}
.od-setup-step{background:#f8faff;border:1px solid #dbeafe;border-radius:8px;padding:14px;margin-bottom:12px}
.od-setup-step h4{font-size:12px;font-weight:700;color:#1e40af;margin:0 0 6px;display:flex;align-items:center;gap:6px}
.od-setup-step p{font-size:11.5px;color:#475569;margin:0;line-height:1.6}
.od-setup-step code{background:#e0e7ff;color:#3730a3;padding:1px 5px;border-radius:3px;font-size:11px}
</style>
</head>
<body>
<div class="login-screen" id="loginScreen">
<div id="jbQuickBanner" style="display:none;position:fixed;top:0;left:0;right:0;background:linear-gradient(135deg,#7c3aed,#6d28d9);color:#fff;padding:10px 20px;font-size:13px;z-index:9999;display:flex;align-items:center;justify-content:center;gap:12px">
<i class="fas fa-circle-exclamation" style="color:#fde68a"></i>
<span>JSONBin not configured — changes will only save locally. <b style="cursor:pointer;text-decoration:underline" onclick="document.getElementById('jbQuickBanner').style.display='none';document.getElementById('loginScreen').classList.add('hidden');document.getElementById('appShell').classList.remove('hidden');openSyncSetup()">Click here to connect</b></span>
<button onclick="document.getElementById('jbQuickBanner').style.display='none'" style="background:none;border:none;color:rgba(255,255,255,.7);cursor:pointer;font-size:16px;margin-left:8px">&#x2715;</button>
</div>
<div class="login-box">
<div class="login-hdr">
<div class="login-logo"><i class="fas fa-crown"></i></div>
<div style="font-family:'Playfair Display',serif;font-size:20px;color:#fff;font-weight:700">Pinnacle</div>
<div style="font-size:10px;color:var(--gold);letter-spacing:2px;text-transform:uppercase;margin-top:3px">Decision Portal</div>
</div>
<div class="login-body">
<div id="loginErr" class="login-err hidden"></div>
<label>Username</label>
<input type="text" id="loginUser" placeholder="Enter username">
<label>Password</label>
<input type="password" id="loginPass" placeholder="Enter password">
<button class="login-btn" id="loginBtn">Sign In &nbsp;<i class="fas fa-arrow-right"></i></button>
<div style="text-align:center;font-size:11px;color:var(--g400);margin-top:14px">Default: <strong>admin</strong> / <strong>Admin</strong></div>
</div>
</div>
</div>
<div id="appShell" class="hidden">
<aside class="sidebar" id="sidebar">
<div class="sb-brand">
<div class="sb-icon"><i class="fas fa-crown"></i></div>
<div><div class="sb-title">Pinnacle</div><div class="sb-sub">Decision Portal</div></div>
</div>
<div class="nav-sec">
<div class="nav-lbl">Navigation</div>
<div class="nav-item" id="nav-dashboard" onclick="showPage(&quot;dashboard&quot;)"><i class="fas fa-chart-pie nav-icon"></i>Project Dashboard</div>
<div class="nav-item" id="nav-phases" onclick="showPage(&quot;phases&quot;)"><i class="fas fa-layer-group nav-icon"></i>Phases &amp; Milestones</div>
<div class="nav-item" id="nav-decisions" onclick="showPage(&quot;decisions&quot;)"><i class="fas fa-gavel nav-icon"></i>Decisions</div>
<div class="nav-item" id="nav-committees" onclick="showPage(&quot;committees&quot;)"><i class="fas fa-people-roof nav-icon"></i>PMT &amp; PSC</div>
<div class="nav-item" id="nav-meetings" onclick="showPage(&quot;meetings&quot;)"><i class="fas fa-calendar-check nav-icon"></i>Meetings</div>
<div class="nav-item" id="nav-stakeholders" onclick="showPage(&quot;stakeholders&quot;)"><i class="fas fa-handshake nav-icon"></i>Stakeholders</div>
<div class="nav-item" id="nav-risks" onclick="showPage(&quot;risks&quot;)"><i class="fas fa-shield-halved nav-icon"></i>Risk Register</div>
</div>
<div class="nav-sec" id="adminNav" style="margin-top:6px">
<div class="nav-lbl">Administration</div>
<div class="nav-item" id="nav-users" onclick="showPage(&quot;users&quot;)"><i class="fas fa-users-gear nav-icon"></i>User Management</div>
<div class="nav-item" id="nav-logs" onclick="showPage(&quot;logs&quot;)"><i class="fas fa-file-lines nav-icon"></i>Activity Logs</div>
</div>
<div class="sb-foot">
<div class="user-card">
<div class="user-av" id="userAv">A</div>
<div style="flex:1;min-width:0"><div class="user-name" id="userName">Admin</div><div class="user-role" id="userRole">admin</div></div>
<span onclick="doLogout()" style="color:rgba(255,255,255,.4);cursor:pointer;font-size:13px;margin-left:auto;padding:4px" title="Logout"><i class="fas fa-arrow-right-from-bracket"></i></span>
</div>
<div class="sync-bar" id="syncStatusBar" onclick="openSyncSetup()" title="Click to configure live sync">
<span class="sync-dot local" id="emailDot"></span> <span id="emailSyncLbl">Email: Off</span>
</div>
<div class="jb-status-bar" id="jbStatusBar" onclick="openSyncSetup()" title="JSONBin Sync — click to configure">
<span class="jb-dot off"></span><span class="jb-lbl">Sync: Off</span>
</div>
<div class="od-bar" id="odSideBar" onclick="openSyncSetup()">
<span class="od-dot disconnected"></span><span class="od-txt">Sync: Off</span>
</div>
</div>
</div>
</aside>
<div class="main-wrap">
<header class="topbar">
<div class="flex ic gap-2">
<button class="tb-btn" id="mobBtn" onclick="document.getElementById('sidebar').classList.toggle('open')" style="display:none"><i class="fas fa-bars"></i></button>
<h1 class="pg-title" id="pageTitle">Dashboard</h1>
</div>
<div class="tb-right">
<button class="tb-btn" onclick="openSyncSetup()" title="Live Sync & Email Setup" id="cloudBtn"><i class="fas fa-cloud-arrow-up"></i></button>
<button class="tb-btn" onclick="openIE()" title="Import / Export Data"><i class="fas fa-exchange-alt"></i></button>
<div style="position:relative">
<button class="tb-btn" onclick="toggleND()" id="ndBtn"><i class="fas fa-bell"></i><span class="ndot" id="ndCount" style="display:none">0</span></button>
<div class="nd" id="ndDrop">
<div class="nd-hdr"><strong style="font-size:12.5px">Notifications</strong><button onclick="markAllRead()" class="btn btn-xs btn-outline">Mark all read</button></div>
<div id="ndList"></div>
</div>
</div>
<button class="tb-btn" onclick="showPage(&quot;profile&quot;)" title="Profile"><i class="fas fa-user"></i></button>
</div>
</header>
<main class="content" id="mainContent"></main>
</div>
</div>
<script>
var DB=null,CU=null,pc=null,sc=null;
/* ═══════════════════════════════════════════════════
JSONBIN.IO SYNC ENGINE
Free shared database — all users read/write the
same JSON bin. Changes visible to everyone instantly.
Setup: create free account at jsonbin.io, create a
bin, paste the Bin ID and API key in Settings.
═══════════════════════════════════════════════════ */
var _JB = {
enabled: false,
binId: '',
apiKey: '',
pollTimer: null,
lastVer: 0,
syncing: false,
BASE: 'https://api.jsonbin.io/v3'
};
function jbSetStatus(state, text){
var bar = document.getElementById('jbStatusBar');
if(!bar) return;
var dot = bar.querySelector('.jb-dot');
var lbl = bar.querySelector('.jb-lbl');
if(dot) dot.className = 'jb-dot ' + state;
if(lbl) lbl.textContent = text || (state==='live'?'Synced':state==='syncing'?'Syncing...':state==='error'?'Sync Error':'Sync: Off');
}
async function jbSave(data){
if(!_JB.binId || !_JB.apiKey) return;
if(_JB.syncing) return;
_JB.syncing = true;
jbSetStatus('syncing','Saving...');
try{
// Ensure we never send null/empty — JSONBin rejects blank bodies
var payload = data || DB || {};
var body = JSON.stringify(payload);
if(!body || body === 'null' || body === '{}') throw new Error('No data to save');
var r = await fetch(_JB.BASE+'/b/'+_JB.binId, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Master-Key': _JB.apiKey,
'X-Bin-Versioning': 'false'
},
body: body
});
if(r.status===401) throw new Error('Invalid API key — check your Master Key');
if(r.status===404) throw new Error('Bin not found — check your Bin ID');
if(!r.ok){
var errTxt = await r.text().catch(function(){return '';});
throw new Error('HTTP '+r.status+(errTxt?' — '+errTxt.slice(0,80):''));
}
var j = await r.json();
_JB.lastVer = j.metadata ? j.metadata.version : _JB.lastVer;
_JB.enabled = true;
jbSetStatus('live','Synced');
showSyncToast('Saved — all users updated');
} catch(e){
console.warn('JSONBin save error:',e);
jbSetStatus('error','Save failed');
flash('Sync failed: '+e.message+'. Check your Bin ID and Master Key in the sync settings.','danger');
}
_JB.syncing = false;
}
async function jbLoad(){
if(!_JB.binId || !_JB.apiKey) return null;
try{
var r = await fetch(_JB.BASE+'/b/'+_JB.binId+'/latest', {
headers:{'X-Master-Key': _JB.apiKey}
});
if(r.status===404) return null;
if(!r.ok) throw new Error('HTTP '+r.status);
var j = await r.json();
_JB.lastVer = j.metadata ? j.metadata.version : 0;
return j.record || null;
} catch(e){
console.warn('JSONBin load error:',e);
return null;
}
}
function jbStartPolling(){
if(_JB.pollTimer) clearInterval(_JB.pollTimer);
_JB.pollTimer = setInterval(async function(){
if(!_JB.enabled || _JB.syncing) return;
try{
var r = await fetch(_JB.BASE+'/b/'+_JB.binId+'/latest', {
headers:{'X-Master-Key':_JB.apiKey}
});
if(!r.ok) return;
var j = await r.json();
var ver = j.metadata ? j.metadata.version : 0;
if(ver > _JB.lastVer){
_JB.lastVer = ver;
var remote = j.record;
if(!remote) return;
DB = remote;
if(!DB.ids) DB.ids={user:2,phase:5,milestone:30,activity:100,deliverable:1,decision:1,risk:1,comment:1,log:1,notification:1,meeting:1,stakeholder:100};
localStorage.setItem(SK, JSON.stringify(DB));
if(CU){
var pg = document.querySelector('.nav-item.active');
showPage(pg ? pg.id.replace('nav-','') : 'dashboard');
renderNC();
showSyncToast('Dashboard updated by another user');
}
}
} catch(e){}
}, 10000); // poll every 10 seconds
}
function jbStopPolling(){
if(_JB.pollTimer){ clearInterval(_JB.pollTimer); _JB.pollTimer=null; }
}
async function jbConnect(binId, apiKey){
if(!binId || !apiKey){flash('Please enter both the Bin ID and Master Key.','danger');return;}
_JB.binId = binId.trim();
_JB.apiKey = apiKey.trim();
_JB.enabled = false;
jbSetStatus('syncing','Connecting...');
try{
// Try to read existing bin data
var readR = await fetch(_JB.BASE+'/b/'+_JB.binId+'/latest', {
headers:{'X-Master-Key': _JB.apiKey, 'X-Access-Key': _JB.apiKey}
});
if(readR.status===401||readR.status===403) throw new Error('Invalid Master Key. Copy the X-MASTER-KEY from JSONBin Account page.');
if(readR.status===404) throw new Error('Bin not found. Double-check your Bin ID.');
var j = await readR.json();
var remote = j.record || j;
if(remote && remote.users && Array.isArray(remote.users) && remote.users.length>0){
// Remote has data — use it
DB = remote;
if(!DB.ids)DB.ids={user:2,phase:5,milestone:30,activity:100,deliverable:1,decision:1,risk:1,comment:1,log:1,notification:1,meeting:1,stakeholder:100};
localStorage.setItem(SK,JSON.stringify(DB));
_JB.lastVer = j.metadata?j.metadata.version:0;
} else {
// Bin is empty or has no real data — push our local DB
if(!DB||!DB.users){loadDB();}
var pushR = await fetch(_JB.BASE+'/b/'+_JB.binId,{
method:'PUT',
headers:{'Content-Type':'application/json','X-Master-Key':_JB.apiKey,'X-Bin-Versioning':'false'},
body: JSON.stringify(DB)
});
if(!pushR.ok){
var et = await pushR.text().catch(function(){return '';});
throw new Error('Write failed: HTTP '+pushR.status+' — '+et.slice(0,80));
}
var pj = await pushR.json();
_JB.lastVer = pj.metadata?pj.metadata.version:0;
}
// Save config and activate
_JB.enabled = true;
DB._jbBinId = _JB.binId;
DB._jbApiKey = _JB.apiKey;
localStorage.setItem(SK,JSON.stringify(DB));
jbSetStatus('live','Synced');
jbStartPolling();
updateSyncUI();
var m=document.getElementById('syncModal');if(m)m.remove();
flash('<i class="fas fa-check-circle"></i> Connected! Changes now sync to all users every 10 seconds.','success');
var pg=document.querySelector('.nav-item.active');
showPage(pg?pg.id.replace('nav-',''):'dashboard');
} catch(e){
_JB.enabled=false;_JB.binId='';_JB.apiKey='';
jbSetStatus('error','Failed');
flash('Connection failed: '+e.message,'danger');
var btn=document.querySelector('#syncModal .btn-primary');
if(btn){btn.disabled=false;btn.innerHTML='<i class="fas fa-plug"></i> Connect JSONBin';}
}
}
async function jbLoadOnLogin(){
if(!_JB.enabled || !_JB.binId) return;
jbSetStatus('syncing','Loading latest...');
try{
var remote = await jbLoad();
if(remote && Object.keys(remote).length > 0){
DB = remote;
if(!DB.ids) DB.ids={user:2,phase:5,milestone:30,activity:100,deliverable:1,decision:1,risk:1,comment:1,log:1,notification:1,meeting:1,stakeholder:100};
localStorage.setItem(SK, JSON.stringify(DB));
}
jbSetStatus('live','Synced');
} catch(e){
jbSetStatus('error','Load failed');
}
}
/* =========================================================
SYNC ENGINE — localStorage + optional manual export/import
Firebase removed. Use Netlify hosting + shared URL for
multi-user access. Data persists in each user's browser.
For true multi-user sync, use the Export/Import feature.
========================================================= */
var _FB = {enabled:false,ref:null}; // stub so existing refs don't break
function fbSave(data){ localStorage.setItem(SK,JSON.stringify(data)); }
function fbConnect(){ flash('Live sync via Firebase has been removed. Share the portal URL on Netlify for multi-user access.','danger'); }
function fbDisconnect(){}
function fbStartListening(){}
async function fbLoadOnce(){ return null; }
async function odLoadOnLogin(){}
function odSave(data){ fbSave(data); }
function odStartPolling(){}
function spSave(data){ fbSave(data); }
function spStartPolling(){}
var SK='pinnacle_v3';
var PC=['#1a3a5c','#c9a227','#2e6da4','#8b6914','#15803d','#7c3aed'];
function defaultDB(){
var n=new Date().toISOString();
var phases=[
{id:1,num:1,name:'Market Intelligence & Insights / Diagnostic & Discovery',desc:'Initial research, stakeholder surveys, and market diagnostic activities.',order:1},
{id:2,num:2,name:'Dialogue & Expert Engagement',desc:'Stakeholder validation, internal performance analysis, and comparative assessment.',order:2},
{id:3,num:3,name:'Strategy Formulation & Quantification',desc:'Co-creation of strategic vision, frameworks, and business plan development.',order:3},
{id:4,num:4,name:'Strategy Finalization & Issuance',desc:'Management presentation, revisions, BOD approval, and final closeout.',order:4}
];
var raw=[
[1,1,'Project Kick-off','1',null,['Kick-off Meeting','Project Plan']],
[1,2,'Quantitative & Qualitative Research Plan','1','2026-06-01',['Inception Report','Communication Strategy and Survey Roll-out Plan']],
[1,3,'Desktop Research','1-2',null,['Desktop Research Execution']],
[1,4,'Drafting of Stakeholder Surveys','1',null,['Draft Stakeholder Surveys']],
[1,5,'Conduct Interviews','2',null,['Conduct Stakeholder Interviews']],
[1,6,'Prepare Draft Market Diagnostic Report','3',null,['Market Diagnostic Paper','PESTEL & Porter Five Forces Analysis']],
[2,7,'Validate Diagnostic Findings','3',null,['Completion of Research','Common Understanding Among Stakeholders','Stakeholder Interviews and Workshops Outputs']],
[2,8,'Internal Performance Analysis','4',null,['Internal Performance Review']],
[2,9,'SWOT Analysis','5',null,['SWOT Analysis Report']],
[2,10,'Comparative Analysis','5',null,['Competitive Analysis Report']],
[3,11,'Co-Create Strategic Vision','6',null,['Draft Strategic Vision']],
[3,12,'Design Operational Risk Framework, Branding & Development Strategies','6',null,['Risk and Compliance Framework','Branding Strategy','Training and Skills Development Plan']],
[3,13,'Develop Business Plan with Financial Projections','7','2026-12-15',['Draft Business Plan','Financial Model Based on Assumptions','Draft Implementation Roadmap']],
[4,14,'Present Strategic Business Plan to Management','7',null,['Presentation to the Management']],
[4,15,'Incorporate Changes as per Management Feedback','9',null,['Validated Strategic Business Plan Incorporating Management Feedback']],
[4,16,'Present Updated Strategic Business Plan to BOD & Closeout','9','2027-01-15',['Final Business Plan Along with Financial Projection','Presentation for BOD']]
];
var mid=1,aid=1,milestones=[],activities=[];
for(var i=0;i<raw.length;i++){
var r=raw[i];
var isKO=(r[2]==='Project Kick-off');
milestones.push({id:mid,phaseId:r[0],serial:r[1],title:r[2],month:r[3],tentDate:r[4],startDate:null,endDate:r[4],status:isKO?'completed':'not_started',priority:'medium',owner:'',notes:'',desc:'',createdAt:n});
for(var j=0;j<r[5].length;j++){
activities.push({id:aid++,milestoneId:mid,title:r[5][j],month:r[3],tentDate:r[4],startDate:null,endDate:r[4],status:isKO?'completed':'not_started',progress:isKO?100:0,assignedTo:'',notes:'',desc:'',createdAt:n});
}
mid++;
}
return{
users:[{id:1,username:'admin',password:'Admin',role:'admin',fullName:'System Administrator',email:'admin@pinnacle.local',dept:'Administration',active:true,lastLogin:null}],
projectStart:'2026-01-01',
projectDuration:12,
phases:phases,milestones:milestones,activities:activities,
committees:{
pmt:{
name:'Project Management Team',
abbr:'PMT',
role:'Responsible for day-to-day project management, activity execution, milestone tracking and progress reporting.',
chair:'Project Manager',
members:[],
meetingCadence:'Weekly',
responsibilities:['Activity oversight','Milestone review & endorsement','Risk monitoring & escalation','Progress reporting to PSC','Resource management']
},
psc:{
name:'Project Steering Committee',
abbr:'PSC',
role:'Responsible for strategic decisions, formal approvals of milestones and deliverables, budget authority and project direction.',
chair:'Executive Sponsor',
members:[],
meetingCadence:'Monthly',
responsibilities:['Milestone approval','Deliverable sign-off','Budget approval','Strategic decisions','Issue escalation resolution']
}
},
meetings:[],
projectNarrative:{thisWeek:'',nextWeek:'',keyMessage:'',lastUpdated:'',updatedBy:''},
projectDuration:12,
stakeholders:[
{id:1,name:'Board of Directors',org:'Bank Board',type:'board',engagementLevel:85,status:'active',role:'Ultimate authority & approval',linkedPhases:[1,2,3,4,5],lastContact:'',nextContact:'',notes:'Quarterly briefings required',owner:'CEO'},
{id:2,name:'Project Steering Committee',org:'Internal',type:'internal',engagementLevel:90,status:'active',role:'Strategic decisions & milestone approval',linkedPhases:[1,2,3,4,5],lastContact:'',nextContact:'',notes:'Monthly meetings',owner:'Project Sponsor'},
{id:3,name:'Central Bank / Regulator',org:'Regulatory Body',type:'regulatory',engagementLevel:60,status:'active',role:'Regulatory compliance & approval',linkedPhases:[2,3,4],lastContact:'',nextContact:'',notes:'Regulatory sign-off required before Phase 3 go-live',owner:'Compliance Officer'},
{id:4,name:'Core Banking Vendor',org:'Technology Partner',type:'vendor',engagementLevel:75,status:'active',role:'System delivery & integration',linkedPhases:[3,4],lastContact:'',nextContact:'',notes:'Weekly technical meetings with PMT',owner:'IT Director'},
{id:5,name:'Internal Audit',org:'Bank Internal',type:'internal',engagementLevel:55,status:'monitoring',role:'Audit oversight & control assessment',linkedPhases:[1,2,3,4,5],lastContact:'',nextContact:'',notes:'Quarterly audit reviews',owner:'Chief Audit Officer'},
{id:6,name:'Business Units',org:'Bank Operations',type:'internal',engagementLevel:70,status:'active',role:'Requirements & UAT participation',linkedPhases:[2,3,4],lastContact:'',nextContact:'',notes:'User acceptance testing leads',owner:'COO'}
],
deliverables:[],decisions:[],risks:[],comments:[],logs:[],notifications:[],
ids:{user:2,phase:5,milestone:mid,activity:aid,deliverable:1,decision:1,risk:1,comment:1,log:1,notification:1}
};
}
/* ?? SYNC / STORAGE LAYER ?? */
var _syncMode='local'; // legacy stub
var _fbUnsubscribe=null;
var _lastRemoteHash='';
var _syncPaused=false;
var _emailSettings={}; // userId -> {enabled, email}
function dbHash(data){
try{var s=JSON.stringify(data);var h=0;for(var i=0;i<Math.min(s.length,500);i++){h=(h*31+s.charCodeAt(i))&0xffffffff;}return String(h);}catch(e){return '';}
}
function loadDB(){
try{DB=JSON.parse(localStorage.getItem(SK));}catch(e){DB=null;}
if(!DB)DB=defaultDB();
try{_emailSettings=JSON.parse(localStorage.getItem(SK+'_email'))||{};}catch(e){_emailSettings={};}
// Auto-restore JSONBin config if previously saved
if(DB._jbBinId && DB._jbApiKey){
_JB.binId = DB._jbBinId;
_JB.apiKey = DB._jbApiKey;
_JB.enabled = true;
jbSetStatus('live','Synced');
jbStartPolling();
}
if(DB._jbBinId && DB._jbApiKey){
_JB.binId=DB._jbBinId; _JB.apiKey=DB._jbApiKey; _JB.enabled=true;
jbSetStatus('live','Synced'); jbStartPolling();
}
updateSyncUI();
}
function saveDB(triggerEmailNotif, changeDesc){
localStorage.setItem(SK,JSON.stringify(DB));
// Push to JSONBin if connected
if(_JB.enabled && _JB.binId){ jbSave(DB); }
updateSyncUI();
if(triggerEmailNotif&&changeDesc&&CU){
broadcastEmailNotification(changeDesc);
}
}
function saveEmailSettings(){
localStorage.setItem(SK+'_email', JSON.stringify(_emailSettings));
}
function getUserEmailPref(uid){
return _emailSettings[uid]||{enabled:false,email:''};
}
function setUserEmailPref(uid,enabled,email){
_emailSettings[uid]={enabled:enabled,email:email||''};
saveEmailSettings();
}
function initFirebaseSync(){
if(!window._fbReady||!window._fbOnValue){
flash('Firebase not configured. Using local storage only. See Setup Guide in Settings.','danger');
return;
}
_syncMode='firebase';
// Listen for remote changes
_fbUnsubscribe=window._fbOnValue(function(snapshot){
if(_syncPaused)return;
var remoteData=snapshot.val();
if(!remoteData)return;
var h=dbHash(remoteData);
if(h===_lastRemoteHash)return; // no change
_lastRemoteHash=h;
// Don't overwrite if we just pushed
var remoteStr=JSON.stringify(remoteData);
var localStr=JSON.stringify(DB);
if(remoteStr===localStr)return;
DB=remoteData;
if(!DB.ids)DB.ids={user:2,phase:5,milestone:30,activity:100,deliverable:1,decision:1,risk:1,comment:1,log:1,notification:1};
localStorage.setItem(SK,JSON.stringify(DB));
updateSyncUI();
// Re-render current page if app is open
if(CU){
var activeNav=document.querySelector('.nav-item.active');
var page=activeNav?activeNav.id.replace('nav-',''):'dashboard';
if(page)showPage(page);
renderNC();
showSyncToast('Dashboard updated by another user');
}
});
updateSyncUI();
flash('<i class="fas fa-wifi"></i> Live sync enabled — changes sync instantly across all users.','success');
}
function disconnectFirebase(){
if(_fbUnsubscribe){try{_fbUnsubscribe();}catch(e){}}
_fbUnsubscribe=null;
_syncMode='local';
updateSyncUI();
flash('Switched to local storage.','danger');
}
/* updateSyncUI defined above */
/* showSyncToast defined above */
/* ?? EMAIL NOTIFICATIONS (EmailJS) ?? */
var EMAILJS_SERVICE='YOUR_SERVICE_ID';
var EMAILJS_TEMPLATE='YOUR_TEMPLATE_ID';
function broadcastEmailNotification(changeDescription){
var svc=DB._ejSvc||''; var tpl=DB._ejTpl||''; var pk=DB._ejPk||'';
if(!pk||!svc||!tpl){console.warn('EmailJS not configured');return;}
if(!window.emailjs){console.warn('EmailJS library not loaded');return;}
try{emailjs.init({publicKey:pk});}catch(e){}
var recipients=[];
(DB.users||[]).forEach(function(u){
if(u.id===CU.id)return;
var pref=getUserEmailPref(u.id);
var emailAddr=(pref.email||u.email||'').trim();
if(pref.enabled&&emailAddr&&emailAddr.indexOf('@')>0){
recipients.push({name:u.fullName||u.username,email:emailAddr});
}
});
if(!recipients.length){console.log('No email recipients with notifications enabled');return;}
var sent=0;
recipients.forEach(function(r){
emailjs.send(svc, tpl, {
to_name: r.name,
to_email: r.email,
from_name: 'Pinnacle Decision Portal',
reply_to: CU.email||'',
changed_by: CU.fullName||CU.username,
change_description: changeDescription,
portal_url: window.location.href,
timestamp: new Date().toLocaleString()
}).then(function(){
sent++;
if(sent===1) flash('<i class="fas fa-envelope-circle-check"></i> Email notification sent to '+recipients.length+' user'+(recipients.length>1?'s':'')+'.');
}).catch(function(e){
console.warn('EmailJS send error:',e);
flash('Email failed: '+(e.text||e.message||JSON.stringify(e)),'danger');
});
});
}
/* ?? SYNC SETUP MODAL ?? */
function openSyncSetup(){
var old=document.getElementById('syncModal'); if(old)old.remove();
var jbOn=_JB.enabled&&_JB.binId;
var overlay=document.createElement('div');
overlay.className='overlay open';
overlay.id='syncModal';
overlay.innerHTML='<div class="modal" style="max-width:600px;max-height:88vh;overflow-y:auto">'
+'<div class="modal-hdr"><span class="modal-title"><i class="fas fa-cloud-arrow-up"></i> Sync &amp; Notifications</span>'
+'<div class="modal-body">'
// ── JSONBin Section ──
+'<div class="setup-banner" style="background:linear-gradient(135deg,#7c3aed,#6d28d9);margin-bottom:14px">'
+'<i class="fas fa-database"></i><div>'
+'<strong>JSONBin.io — Live Multi-User Sync</strong> '
+'<span class="od-badge '+(jbOn?'on':'off')+'">'+(jbOn?'Connected':'Not connected')+'</span><br>'
+'Free shared database. All users reading the same Netlify URL will see each others updates within 10 seconds.</div></div>'
+'<div class="od-setup-step">'
+'<h4><i class="fas fa-list-ol" style="color:#7c3aed"></i> Setup (2 minutes)</h4>'
+'<p>'
+'<b>1.</b> Go to <a href="https://jsonbin.io" target="_blank" style="color:#7c3aed;font-weight:700">jsonbin.io</a> and create a free account<br>'
+'<b>2.</b> Click <b>Create Bin</b> — enter any name — paste <code>{&quot;init&quot;:true}</code> as content — click <b>Create</b><br>'
+'<b>3.</b> Copy the <b>Bin ID</b> from the URL bar — it is the long string after <code>/b/</code> (24+ characters)<br>'
+'<b>4.</b> Click your account icon top-right → <b>API Keys</b> → copy the <b>X-MASTER-KEY</b> (starts with <code>$2b$</code>)<br>'
+'<b>5.</b> Paste both below and click <b>Connect JSONBin</b> — setup takes about 30 seconds'
+'</p></div>'
+'<div class="fg"><label><i class="fas fa-hashtag" style="color:#7c3aed"></i> Bin ID</label>'
+'<input type="text" id="jbBinIn" placeholder="64abc123def456..." value="'+(DB._jbBinId||'')+'" style="font-family:monospace"></div>'
+'<div class="fg"><label><i class="fas fa-key" style="color:#7c3aed"></i> Master Key (X-Master-Key)</label>'
+'<input type="text" id="jbKeyIn" placeholder="$2b$10$..." value="'+(DB._jbApiKey||'')+'" style="font-family:monospace"></div>'
+'<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:6px">'
+'<button class="btn btn-primary" style="background:#7c3aed;border-color:#7c3aed" onclick="applyJBConfig()"><i class="fas fa-plug"></i> '+(jbOn?'Re-connect':'Connect JSONBin')+'</button>'
+(jbOn?'<button class="btn btn-danger btn-sm" onclick="jbDisconnect()"><i class="fas fa-unlink"></i> Disconnect</button>':'')
+(jbOn?'<button class="btn btn-outline btn-sm" onclick="jbSave(DB).then(function(){flash(&quot;Pushed!&quot;,&quot;success&quot;)})"><i class="fas fa-upload"></i> Push Now</button>':'')
+'</div>'
+'<hr style="border:none;border-top:1px solid var(--g200);margin:16px 0">'
// ── Email section header ──
+'<div class="modal-title" style="font-size:13px;font-weight:700;color:var(--navy);margin-bottom:12px"><i class="fas fa-envelope tgold"></i> Email Notifications</div>'
+'<button class="modal-close" onclick="document.getElementById(&quot;syncModal&quot;).remove()">&#x2715;</button></div>'
+'<div class="modal-body">'
+'<div class="setup-banner" style="background:linear-gradient(135deg,#1e3a5f,#0ea5e9);margin-bottom:14px">'
+'<i class="fas fa-envelope-circle-check"></i><div>'
+'<strong>EmailJS — Send Notifications from the Portal</strong><br>'
+'Free service. Create an account at <a href="https://emailjs.com" target="_blank" style="color:#93c5fd;font-weight:700">emailjs.com</a>, '
+'set up a service and template, then paste your keys below.<br>'
+'<span style="font-size:11px;opacity:.8">Your EmailJS template must include these variables: '
+'<b>{{to_name}}, {{to_email}}, {{changed_by}}, {{change_description}}, {{timestamp}}</b>. '
+'Set the <b>To Email</b> field in your template to <b>{{to_email}}</b>.</span>'
+'</div></div>'
+'<div class="od-setup-step">'
+'<h4><i class="fas fa-list-ol" style="color:#0ea5e9"></i> Setup Steps</h4>'
+'<p>'
+'<b>1.</b> Sign up free at <a href="https://emailjs.com" target="_blank" style="color:#2563eb">emailjs.com</a><br>'
+'<b>2.</b> Add an Email Service (Gmail, Outlook, etc.) — copy the <b>Service ID</b><br>'
+'<b>3.</b> Create a Template with the variables above — copy the <b>Template ID</b><br>'
+'<b>4.</b> Go to Account → API Keys — copy your <b>Public Key</b><br>'
+'<b>5.</b> Paste all three below and click Save'
+'</p></div>'
+'<div class="fg"><label><i class="fas fa-key" style="color:#0ea5e9"></i> Public Key</label>'
+'<input type="text" id="ejPk" placeholder="user_xxxxxxxxxxxx" value="'+(DB._ejPk||'')+'" style="font-family:monospace"></div>'
+'<div class="fg-2">'
+'<div class="fg"><label>Service ID</label><input type="text" id="ejSvc" placeholder="service_xxxxxxx" value="'+(DB._ejSvc||'')+'" style="font-family:monospace"></div>'
+'<div class="fg"><label>Template ID</label><input type="text" id="ejTpl" placeholder="template_xxxxxxx" value="'+(DB._ejTpl||'')+'" style="font-family:monospace"></div>'
+'</div>'
+'<div style="display:flex;gap:8px;flex-wrap:wrap">'
+'<button class="btn btn-primary" onclick="applyEmailConfig()"><i class="fas fa-save"></i> Save & Activate</button>'
+((DB._ejPk&&DB._ejSvc&&DB._ejTpl)?'<button class="btn btn-outline btn-sm" onclick="testEmailNotif()"><i class="fas fa-paper-plane"></i> Send Test Email</button>':'')
+'</div>'
+'</div>'
+'<div class="modal-foot"><button class="btn btn-outline" onclick="document.getElementById(&quot;syncModal&quot;).remove()">Close</button></div>'
+'</div>';
document.body.appendChild(overlay);
overlay.addEventListener('click',function(e){if(e.target===overlay)overlay.remove();});
}
function applyJBConfig(){
var binId = ((document.getElementById('jbBinIn')||{}).value||'').trim();
var apiKey = ((document.getElementById('jbKeyIn')||{}).value||'').trim();
if(!binId){flash('Bin ID cannot be blank. Copy it from your JSONBin dashboard URL.','danger');return;}
if(!apiKey){flash('Master Key cannot be blank. Copy it from JSONBin Account → API Keys.','danger');return;}
if(binId.length < 10){flash('Bin ID looks too short. It should be around 24 characters.','danger');return;}
if(!apiKey.startsWith('$2')){flash('Master Key format looks incorrect — it should start with $2a$ or $2b$. Please copy it again from JSONBin Account page.','danger');return;}
var btn=document.querySelector('#syncModal .btn-primary');
if(btn){btn.disabled=true;btn.innerHTML='<i class="fas fa-spinner fa-spin"></i> Connecting...';}
jbConnect(binId, apiKey).then(function(){
if(btn){btn.disabled=false;btn.innerHTML='<i class="fas fa-plug"></i> Re-connect JSONBin';}
}).catch(function(e){
if(btn){btn.disabled=false;btn.innerHTML='<i class="fas fa-plug"></i> Connect JSONBin';}
});
}
function applyEmailConfig(){
var pk=(document.getElementById('ejPk')||{}).value||'';
var svc=(document.getElementById('ejSvc')||{}).value||'';
var tpl=(document.getElementById('ejTpl')||{}).value||'';
if(!pk||!svc||!tpl){flash('All three EmailJS fields are required.','danger');return;}
pk=pk.trim();svc=svc.trim();tpl=tpl.trim();
DB._ejPk=pk;DB._ejSvc=svc;DB._ejTpl=tpl;
EMAILJS_SERVICE=svc;EMAILJS_TEMPLATE=tpl;
if(window.emailjs){
try{emailjs.init({publicKey:pk});console.log('EmailJS initialised with key:',pk.slice(0,8)+'...');}
catch(e){flash('EmailJS init error: '+e.message,'danger');return;}
} else {flash('EmailJS library failed to load. Check your internet connection.','danger');return;}
saveDB();
flash('<i class="fas fa-envelope-circle-check"></i> Email config saved & activated! Go to Profile → Email Notifications to enable alerts.','success');
}
/* applyODConfig removed, replaced by spConnect() */
function updateSyncUI(){
var emailOn = !!(DB._ejPk&&DB._ejSvc&&DB._ejTpl);
var jbOn = _JB.enabled && !!_JB.binId;
// Top bar
var el = document.getElementById('syncStatusBar');
if(el) el.innerHTML = '<span class="sync-dot '+(jbOn?'live':'local')+'"></span>'+(jbOn?'Live Sync':'Local');
// Email indicator
var dot=document.getElementById('emailDot'),lbl=document.getElementById('emailSyncLbl');
if(dot) dot.className='sync-dot '+(emailOn?'live':'local');
if(lbl) lbl.textContent = emailOn?'Email: On':'Email: Off';
// JSONBin indicator
jbSetStatus(jbOn?'live':'off', jbOn?'Live Sync':'Sync: Off');
}
function updateProjStart(val){
if(!val)return;
DB.projectStart = val+'-01';
saveDB();
// Re-render Gantt
var phases=DB.phases.filter(function(p){return DB.milestones.some(function(m){return m.phaseId===p.id;});}).sort(function(a,b){return a.order-b.order;});
renderGantt(phases);
flash('Project start date updated to '+val);
}
function nid(t){return DB.ids[t]++;}
function isAdmin(){return CU&&CU.role==='admin';}
function isEditor(){return CU&&(CU.role==='admin'||CU.role==='editor');}
function today(){return new Date().toISOString().slice(0,10);}
function addDays(d,n){var dt=new Date(d);dt.setDate(dt.getDate()+n);return dt.toISOString().slice(0,10);}
function isDelayed(ed,st){return ed&&ed<today()&&st!=='completed';}
function addLog(action,et,eid,en,det){
DB.logs.push({id:nid('log'),userId:CU?CU.id:0,action:action,entityType:et||'',entityId:eid||0,entityName:en||'',details:det||'',timestamp:new Date().toISOString()});
saveDB();
}
function addNotif(uid,title,msg,type){
DB.notifications.push({id:nid('notification'),userId:uid,title:title,message:msg||'',type:type||'info',read:false,createdAt:new Date().toISOString()});
saveDB();renderNC();
}
function sBadge(s){
var m={completed:'b-success',in_progress:'b-info',delayed:'b-danger',not_started:'b-gray',pending:'b-warn',approved:'b-success',rejected:'b-danger',open:'b-danger',mitigated:'b-info',closed:'b-success',deferred:'b-gray',on_hold:'b-gray',in_review:'b-warn',accepted:'b-gray'};
return '<span class="badge '+(m[s]||'b-gray')+'">'+s.replace(/_/g,' ').replace(/\b\w/g,function(c){return c.toUpperCase();})+'</span>';
}
function pBadge(p){
var m={low:'b-gray',medium:'b-warn',high:'b-danger',critical:'b-danger'};
return '<span class="badge '+(m[p]||'b-gray')+'">'+p.charAt(0).toUpperCase()+p.slice(1)+'</span>';
}
function fIcon(ft){
if(ft==='pdf')return 'fa-file-pdf';
if(ft==='xlsx'||ft==='csv')return 'fa-file-excel';
if(ft==='docx'||ft==='doc')return 'fa-file-word';
if(ft==='pptx'||ft==='ppt')return 'fa-file-powerpoint';
if(ft==='jpg'||ft==='jpeg'||ft==='png')return 'fa-file-image';
return 'fa-file-alt';
}
function flash(msg,type){
type=type||'success';
var d=document.createElement('div');
d.className='alert alert-'+type;
d.innerHTML='<i class="fas fa-'+(type==='success'?'check-circle':'exclamation-circle')+'"></i>'+msg+'<span style="margin-left:auto;cursor:pointer" onclick="this.parentElement.remove()">&#x2715;</span>';
var mc=document.getElementById('mainContent');
if(mc)mc.prepend(d);
setTimeout(function(){if(d.parentNode)d.remove();},3500);
}
function g(id){return document.getElementById(id);}
function gv(id){var el=g(id);return el?el.value:'';}
function pct(a,b){return a?Math.round(b/a*1000)/10:0;}
function phasePct(pid){
var mids=DB.milestones.filter(function(m){return m.phaseId===pid;}).map(function(m){return m.id;});
var acts=DB.activities.filter(function(a){return mids.indexOf(a.milestoneId)>=0;});
return pct(acts.length,acts.filter(function(a){return a.status==='completed';}).length);
}
function milestonePct(mid){
var mil=DB.milestones.find(function(m){return m.id===mid;});
if(mil&&mil.status==='completed')return 100;
var acts=DB.activities.filter(function(a){return a.milestoneId===mid;});
if(!acts.length)return(mil&&mil.status==='in_progress')?50:0;
var done=acts.filter(function(a){return a.status==='completed';}).length;
return Math.round(done/acts.length*100);
}
/* ?? AUTH ?? */
function doLogin(){
var u=g('loginUser').value.trim(),p=g('loginPass').value;
if(_JB.enabled && _JB.binId){
var btn=g('loginBtn');
if(btn){btn.disabled=true;btn.innerHTML='<i class="fas fa-spinner fa-spin"></i> Loading...';}
jbLoadOnLogin().then(function(){
_doLoginCore(u,p);
if(btn){btn.disabled=false;btn.innerHTML='<i class="fas fa-sign-in-alt"></i> Sign In';}
}).catch(function(){
_doLoginCore(u,p);
if(btn){btn.disabled=false;btn.innerHTML='<i class="fas fa-sign-in-alt"></i> Sign In';}
});
} else {
_doLoginCore(u,p);
}
}
function _doLoginCore(u,p){
var user=null;
for(var i=0;i<DB.users.length;i++){if(DB.users[i].username===u&&DB.users[i].password===p&&DB.users[i].active){user=DB.users[i];break;}}
if(!user){var el=g('loginErr');el.textContent='Invalid username or password.';el.classList.remove('hidden');return;}
CU=user;addLog('LOGIN','user',CU.id,CU.username);
g('appShell').classList.remove('hidden');g('loginScreen').classList.add('hidden');
g('userName').textContent=user.fullName||user.username;
g('userRole').textContent=user.role;
g('adminNav').style.display=user.role==='admin'?'':'none';
renderNC();showPage('dashboard');
g('loginUser').value='';g('loginPass').value='';
g('loginErr').classList.add('hidden');
if(_FB.enabled)fbStartListening();
}
// Login button event listeners
g('loginBtn').addEventListener('click', doLogin);
g('loginPass').addEventListener('keydown', function(e){ if(e.key==='Enter') doLogin(); });
g('loginUser').addEventListener('keydown', function(e){ if(e.key==='Enter') g('loginPass').focus(); });
function doLogout(){
addLog('LOGOUT','user',CU.id,CU.username);CU=null;
if(pc){pc.destroy();pc=null;}if(sc){sc.destroy();sc=null;}
g('appShell').classList.add('hidden');g('loginScreen').classList.remove('hidden');
}
/* ?? NOTIFICATIONS ?? */
function renderNC(){
if(!CU)return;
var c=DB.notifications.filter(function(n){return n.userId===CU.id&&!n.read;}).length;
var el=g('ndCount');el.textContent=c>9?'9+':c;el.style.display=c>0?'flex':'none';
}
function toggleND(){
var d=g('ndDrop');d.classList.toggle('open');
if(d.classList.contains('open'))renderNL();
}
function renderNL(){
var items=DB.notifications.filter(function(n){return n.userId===CU.id;}).sort(function(a,b){return b.createdAt.localeCompare(a.createdAt);}).slice(0,30);
g('ndList').innerHTML=items.length?items.map(function(n){
return '<div class="nd-item'+(n.read?'':' unread')+'" onclick="openNotif('+n.id+')">'
+'<div style="font-weight:'+(n.read?500:700)+';font-size:13px">'+n.title+'</div>'
+'<div class="muted" style="font-size:11.5px;margin-top:2px">'+(n.message||'').slice(0,70)+(n.message&&n.message.length>70?'...':'')+'</div>'
+'<div class="flex" style="gap:8px;margin-top:4px"><span class="muted" style="font-size:11px">'+n.createdAt.slice(0,16).replace('T',' ')+'</span>'
+(n.read?'':'<span style="font-size:10px;background:var(--gold);color:var(--navy);padding:1px 6px;border-radius:8px;font-weight:700">NEW</span>')
+'</div></div>';
}).join(''):'<div style="padding:20px;text-align:center;color:var(--g400);font-size:13px">No notifications</div>';
}
function markNR(id){var n=DB.notifications.find(function(x){return x.id===id;});if(n)n.read=true;saveDB();renderNC();renderNL();}
function openNotif(id){
var n=DB.notifications.find(function(x){return x.id===id;});
if(!n)return;
n.read=true;saveDB();renderNC();renderNL();
var old=document.getElementById('notifPopup');
if(old)old.remove();
var overlay=document.createElement('div');
overlay.className='overlay open';
overlay.id='notifPopup';
var modal=document.createElement('div');
modal.className='modal';
modal.style.maxWidth='480px';
var hdr=document.createElement('div');
hdr.className='modal-hdr';
hdr.innerHTML='<span class="modal-title"><i class="fas fa-bell"></i> Notification</span>';
var xb=document.createElement('button');
xb.className='modal-close';
xb.innerHTML='&#x2715;';
xb.onclick=function(){var p=document.getElementById('notifPopup');if(p)p.remove();};
hdr.appendChild(xb);
modal.appendChild(hdr);
var body=document.createElement('div');
body.className='modal-body';
var msgBody=n.message||'No message body.';
body.innerHTML='<div style="font-size:16px;font-weight:700;color:#0d2137;margin-bottom:6px">'+n.title+'</div>'
+'<div style="font-size:12px;color:#a1a1aa;margin-bottom:12px">'+n.createdAt.slice(0,16).replace('T',' ')
+'&nbsp;&nbsp;<span style="background:#f4f4f5;color:#52525b;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:600">'+n.type+'</span></div>'
+'<div style="font-size:14px;line-height:1.6;background:#fafafa;padding:14px;border-radius:8px;border:1px solid #e4e4e7;margin-bottom:14px;color:#27272a">'+msgBody+'</div>'
+'<div style="border-top:1px solid #e4e4e7;padding-top:12px">'
+'<label style="display:block;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#52525b;margin-bottom:6px">Reply / Note</label>'
+'<textarea id="notifReply" rows="3" placeholder="Type a reply or note..." style="width:100%;padding:8px 10px;border:1px solid #e4e4e7;border-radius:6px;font-size:13px;resize:vertical;font-family:inherit"></textarea>'
+'<div id="notifBtnRow" style="display:flex;gap:8px;margin-top:8px"></div>'
+'</div>';
modal.appendChild(body);
overlay.appendChild(modal);
document.body.appendChild(overlay);
var row=document.getElementById('notifBtnRow');
var b1=document.createElement('button');b1.className='btn btn-primary btn-sm';b1.innerHTML='<i class="fas fa-paper-plane"></i> Send Reply';
b1.onclick=function(){sendNotifReply(id);};row.appendChild(b1);
var b2=document.createElement('button');b2.className='btn btn-danger btn-sm';b2.innerHTML='<i class="fas fa-trash"></i> Delete';
b2.onclick=function(){deleteNotif(id);};row.appendChild(b2);
var b3=document.createElement('button');b3.className='btn btn-outline btn-sm';b3.textContent='Close';
b3.onclick=function(){var p=document.getElementById('notifPopup');if(p)p.remove();};row.appendChild(b3);
overlay.addEventListener('click',function(e){if(e.target===overlay)overlay.remove();});
}
function sendNotifReply(id){
var txt=g('notifReply')?g('notifReply').value.trim():'';
if(!txt){alert('Please type a reply first.');return;}
var n=DB.notifications.find(function(x){return x.id===id;});
addNotif(CU.id,'Reply: '+(n?n.title:'Notification'),'Your reply: '+txt,'info');
addLog('NOTIF_REPLY','notification',id,'Reply sent');
saveDB();if(g('notifPopup'))g('notifPopup').remove();flash('Reply saved as notification.');
}
function deleteNotif(id){
DB.notifications=DB.notifications.filter(function(x){return x.id!==id;});
saveDB();renderNC();renderNL();if(g('notifPopup'))g('notifPopup').remove();flash('Notification deleted.');
}
function markAllRead(){DB.notifications.filter(function(n){return n.userId===CU.id;}).forEach(function(n){n.read=true;});saveDB();renderNC();renderNL();}
document.addEventListener('click',function(e){
var d=g('ndDrop'),b=g('ndBtn');
if(d&&b&&!d.contains(e.target)&&!b.contains(e.target))d.classList.remove('open');
});
/* ?? ROUTING ?? */
function showPage(page,param){
if(pc){pc.destroy();pc=null;}if(sc){sc.destroy();sc=null;}
document.querySelectorAll('.nav-item').forEach(function(n){n.classList.remove('active');});
var nav=g('nav-'+page);if(nav)nav.classList.add('active');
var titles={dashboard:'Project Dashboard',phases:'Phases & Milestones',decisions:'Decision Register',risks:'Risk Register',users:'User Management',logs:'Activity Logs',profile:'My Profile',committees:'PMT & PSC Oversight',meetings:'Committee Meetings',stakeholders:'Stakeholder Engagement Tracker'};
g('pageTitle').textContent=titles[page]||'Pinnacle';
g('mainContent').innerHTML='';
if(page==='dashboard')renderDashboard();
else if(page==='phases')renderPhases();
else if(page==='phaseDetail')renderPhaseDetail(param);
else if(page==='milestoneDetail')renderMilestoneDetail(param);
else if(page==='decisions')renderDecisions();
else if(page==='risks')renderRisks();
else if(page==='users')renderUsers();
else if(page==='logs')renderLogs();
else if(page==='profile')renderProfile();
else if(page==='committees')renderCommittees();
else if(page==='meetings')renderMeetings();
else if(page==='stakeholders')renderStakeholders();
}
function openModal(id){var m=g(id);if(m)m.classList.add('open');}
function closeModal(id){var m=g(id);if(m)m.classList.remove('open');}
document.addEventListener('click',function(e){if(e.target.classList.contains('overlay'))e.target.classList.remove('open');});
/* ?? DASHBOARD ?? */
function renderBlockers(){
var td=today();
var blockers=[];
// SOURCE 1: Overdue activities (past endDate, not completed)
DB.activities.forEach(function(a){
if(a.status==='completed'||!a.endDate)return;
if(a.endDate>=td)return;
var daysLate=Math.round((new Date(td)-new Date(a.endDate))/(1000*60*60*24));
var mil=DB.milestones.find(function(m){return m.id===a.milestoneId;})||{};
var ph=DB.phases.find(function(p){return p.id===mil.phaseId;})||{};
blockers.push({
type:'activity',label:'Overdue Activity',name:a.title,
meta:'Phase '+(ph.num||'?')+' · '+(mil.title||''),
daysLate:daysLate,owner:a.owner||'Unassigned',
sev:daysLate>14?'critical':daysLate>7?'high':'medium',
score:daysLate*3,link:'phases'
});
});
// SOURCE 2: Open high/critical risks
DB.risks.forEach(function(r){
if(r.status!=='open')return;
var sev=(r.severity||r.impact||'medium').toLowerCase();
if(sev!=='high'&&sev!=='critical'&&sev!=='very high')return;
var daysOpen=0;
if(r.dateRaised){daysOpen=Math.round((new Date(td)-new Date(r.dateRaised))/(1000*60*60*24));}
blockers.push({
type:'risk',label:'Open Risk',name:r.title,
meta:'Severity: '+(r.severity||'High')+' · Owner: '+(r.owner||'Unassigned'),
daysLate:daysOpen,owner:r.owner||'Unassigned',
sev:sev==='critical'?'critical':'high',
score:daysOpen*2+(sev==='critical'?100:50),link:'risks'
});
});
// SOURCE 3: Pending decisions overdue (older than 7 days)
DB.decisions.forEach(function(d){
if(d.status!=='pending')return;
var daysOpen=0;
if(d.dateRaised||d.created){
daysOpen=Math.round((new Date(td)-new Date(d.dateRaised||d.created))/(1000*60*60*24));
}
if(daysOpen<7)return;
blockers.push({
type:'decision',label:'Pending Decision',name:d.title,
meta:'Awaiting approval · '+(d.owner||''),
daysLate:daysOpen,owner:d.owner||'Unassigned',
sev:daysOpen>21?'critical':daysOpen>14?'high':'medium',
score:daysOpen*2+(d.priority==='high'?40:0),link:'decisions'
});
});
// Sort by score desc, take top 3
blockers.sort(function(a,b){return b.score-a.score;});
var top=blockers.slice(0,3);
var sevConfig={
critical:{col:'#f43f5e',bg:'rgba(244,63,94,.15)',txt:'CRITICAL',numBg:'#f43f5e',numCol:'#fff'},
high: {col:'#f97316',bg:'rgba(249,115,22,.12)',txt:'HIGH', numBg:'#f97316',numCol:'#fff'},
medium: {col:'#eab308',bg:'rgba(234,179,8,.10)', txt:'MEDIUM', numBg:'#eab308',numCol:'#fff'}
};
var typeConfig={
activity:{pill:'tp-activity',icon:'fa-bolt'},
risk: {pill:'tp-risk', icon:'fa-shield-exclamation'},
decision:{pill:'tp-decision',icon:'fa-gavel'}
};
var daysClass=function(d){return d>14?'':d>7?' orange':' amber';};
var inner='';
if(!top.length){
inner='<div class="no-blockers"><i class="fas fa-shield-check"></i>No active blockers — project is on track!</div>';
} else {
// Pad to 3 items with placeholder
while(top.length<3){
top.push(null);
}
inner='<div class="blocker-items">';
top.forEach(function(b,i){
if(!b){
inner+='<div class="blocker-item" style="opacity:.3;cursor:default">'
+'<div class="blocker-rank"><div class="blocker-rank-num" style="background:rgba(255,255,255,.1);color:rgba(255,255,255,.3)">'+(i+1)+'</div>'
+'<span style="color:rgba(255,255,255,.25);font-size:9px">No blocker</span></div>'
+'<div class="blocker-name" style="color:rgba(255,255,255,.2)">All clear</div>'
+'</div>';
return;
}
var sc=sevConfig[b.sev]||sevConfig.medium;
var tc=typeConfig[b.type]||typeConfig.activity;
var dCls=b.daysLate>14?'':b.daysLate>7?' orange':' amber';
var dLabel=b.daysLate===0?'Today':b.daysLate===1?'1 day':b.daysLate+' days';
var dType=b.type==='activity'?'overdue':b.type==='risk'?'open':'pending';
inner+='<div class="blocker-item sev-'+b.sev+'" onclick="showPage(&quot;'+b.link+'&quot;)" title="Click to view '+b.label+'">'
+'<div class="blocker-rank">'
+'<div class="blocker-rank-num" style="background:'+sc.numBg+';color:'+sc.numCol+'">'+(i+1)+'</div>'
+'<span class="blocker-type-pill '+tc.pill+'"><i class="fas '+tc.icon+'" style="margin-right:3px"></i>'+b.label+'</span>'
+'</div>'
+'<div class="blocker-name">'+b.name+'</div>'
+'<div class="blocker-meta">'+b.meta+'</div>'
+'<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:6px">'
+'<div class="blocker-days'+(b.daysLate>14?'':b.daysLate>7?' orange':' amber')+'"><i class="fas fa-clock" style="font-size:8px"></i> '+dLabel+' '+dType+'</div>'
+'<div class="escalate-btn" onclick="event.stopPropagation();openEscalateModal(&quot;'+b.name.replace(/[&quot;<>]/g,'')+'&quot;,&quot;'+b.owner+'&quot;)"><i class="fas fa-arrow-up-right-from-square" style="font-size:8px"></i> Escalate</div>'
+'</div>'
+'</div>';
});
inner+='</div>';
}
var totalCount=blockers.length;
return '<div class="blockers-panel">'
+'<div class="blockers-hdr">'
+'<div class="blockers-hdr-left">'
+'<div style="width:32px;height:32px;background:rgba(244,63,94,.2);border-radius:9px;display:flex;align-items:center;justify-content:center;border:1px solid rgba(244,63,94,.3)">'
+'<i class="fas fa-radar" style="color:#f43f5e;font-size:14px"></i></div>'
+'<div><div class="blockers-title">Action Intelligence Centre</div>'
+'<div class="blockers-sub">'+totalCount+' item'+(totalCount!==1?'s':'')+' requiring attention · Ranked by priority & urgency</div>'
+'</div></div>'
+(totalCount>3?'<div class="blockers-badge">+' +(totalCount-3)+' more</div>':'')
+'<button class="btn btn-outline btn-sm" style="border-color:rgba(255,255,255,.2);color:rgba(255,255,255,.6);background:rgba(255,255,255,.05)" onclick="showAllBlockers()">'
+'<i class="fas fa-list"></i> View All</button>'
+'</div>'
+inner
+'</div>';
}
function escalateBlocker(name,owner){
var m=document.createElement('div');
m.className='overlay open';
m.innerHTML='<div class="modal" style="max-width:440px">'
+'<div class="modal-hdr"><span class="modal-title" style="color:#f43f5e"><i class="fas fa-arrow-up-right-from-square"></i> Escalate Blocker</span>'
+'<button class="modal-close" onclick="this.closest(&quot;.overlay&quot;).remove()">&#x2715;</button></div>'
+'<div class="modal-body">'
+'<div style="background:#fff8f8;border:1px solid #fecdd3;border-radius:8px;padding:14px;margin-bottom:16px">'
+'<div style="font-weight:700;color:#be123c;margin-bottom:4px"><i class="fas fa-exclamation-circle"></i> '+name+'</div>'
+'<div style="font-size:12px;color:#9f1239">Owner: '+owner+'</div>'
+'</div>'
+'<div class="fg"><label>Escalate to</label>'
+'<select id="escTo"><option>Project Sponsor</option><option>Department Head</option><option>Steering Committee</option><option>CEO / MD</option></select></div>'
+'<div class="fg"><label>Message</label>'
+'<textarea id="escMsg" rows="3" style="resize:vertical" placeholder="Describe the impact and what decision is needed...">This blocker ('+name+') requires your immediate attention as it is impacting project delivery.</textarea></div>'
+'</div>'
+'<div class="modal-foot">'
+'<button class="btn btn-outline" onclick="this.closest(&quot;.overlay&quot;).remove()">Cancel</button>'
+'<button class="btn btn-gold" id="sendEscBtn" onclick="sendEscalation()"><i class="fas fa-paper-plane"></i> Send Escalation</button>'
+'</div></div>';
document.body.appendChild(m);
m.addEventListener('click',function(e){if(e.target===m)m.remove();});
}
function openEscalateModal(name,owner){escalateBlocker(name,owner);}
function sendEscalation(name){
if(!name){
var title=document.querySelector('.overlay.open .modal-title');
name=title?title.textContent.replace('Escalate Blocker','').trim():'Unknown';
}
var to=(document.getElementById('escTo')||{}).value||'';
var msg=(document.getElementById('escMsg')||{}).value||'';
addLog('ESCALATE','blocker',0,'Escalated: '+name+' → '+to);
saveDB();
document.querySelector('.overlay.open').remove();
flash('<i class="fas fa-check-circle"></i> Escalation sent to '+to+' for: '+name,'success');
broadcastEmailNotification('BLOCKER ESCALATED: "'+name+'" has been escalated to '+to+' by '+CU.fullName+'. Message: '+msg);
}
function showAllBlockers(){
var td=today();
var all=[];
DB.activities.forEach(function(a){
if(a.status==='completed'||!a.endDate||a.endDate>=td)return;
var d=Math.round((new Date(td)-new Date(a.endDate))/(1000*60*60*24));
var mil=DB.milestones.find(function(m){return m.id===a.milestoneId;})||{};
all.push({type:'activity',name:a.title,days:d,sev:d>14?'Critical':d>7?'High':'Medium',owner:a.owner||'-'});
});
DB.risks.forEach(function(r){
if(r.status!=='open')return;
var d=r.dateRaised?Math.round((new Date(td)-new Date(r.dateRaised))/(1000*60*60*24)):0;
all.push({type:'risk',name:r.title,days:d,sev:r.severity||'High',owner:r.owner||'-'});
});
DB.decisions.forEach(function(d){
if(d.status!=='pending')return;
var days=d.dateRaised?Math.round((new Date(td)-new Date(d.dateRaised))/(1000*60*60*24)):0;
if(days<7)return;
all.push({type:'decision',name:d.title,days:days,sev:days>21?'Critical':'High',owner:d.owner||'-'});
});
all.sort(function(a,b){return b.days-a.days;});
var rows=all.map(function(b){
var col=b.sev==='Critical'?'#f43f5e':b.sev==='High'?'#f97316':'#eab308';
return '<tr>'
+'<td><span style="font-size:9px;font-weight:700;background:rgba(0,0,0,.06);padding:2px 6px;border-radius:4px">'+b.type.toUpperCase()+'</span></td>'
+'<td style="font-weight:600">'+b.name+'</td>'
+'<td><span style="color:'+col+';font-weight:700">'+b.sev+'</span></td>'
+'<td style="color:#b91c1c;font-weight:700">'+b.days+' days</td>'
+'<td>'+b.owner+'</td>'
+'</tr>';
}).join('');
var m=document.createElement('div');
m.className='overlay open';
m.innerHTML='<div class="modal" style="max-width:680px">'
+'<div class="modal-hdr"><span class="modal-title"><i class="fas fa-triangle-exclamation" style="color:#f43f5e"></i> All Blockers ('+all.length+')</span>'
+'<button class="modal-close" onclick="this.closest(&quot;.overlay&quot;).remove()">&#x2715;</button></div>'
+'<div class="modal-body"><div class="tbl-wrap"><table><thead><tr><th>Type</th><th>Blocker</th><th>Severity</th><th>Days</th><th>Owner</th></tr></thead>'
+'<tbody>'+(rows||'<tr><td colspan="5" style="text-align:center;padding:22px;color:var(--g400)">No blockers found</td></tr>')+'</tbody></table></div></div>'
+'<div class="modal-foot"><button class="btn btn-outline" onclick="this.closest(&quot;.overlay&quot;).remove()">Close</button></div>'
+'</div>';
document.body.appendChild(m);
m.addEventListener('click',function(e){if(e.target===m)m.remove();});
}
function getCommitteeColours(type){
return type==='pmt'
?{bg:'#60a5fa',light:'#dbeafe',dark:'#1d4ed8',pill:'ab-pmt'}
:{bg:'#a78bfa',light:'#ede9fe',dark:'#6d28d9',pill:'ab-psc'};
}
function renderCommitteeBanner(){
var pmt=DB.committees&&DB.committees.pmt?DB.committees.pmt:{name:'Project Management Team',abbr:'PMT',chair:'Project Manager',meetingCadence:'Weekly',members:[]};
var psc=DB.committees&&DB.committees.psc?DB.committees.psc:{name:'Project Steering Committee',abbr:'PSC',chair:'Executive Sponsor',meetingCadence:'Monthly',members:[]};
// Count stats for each committee
var totalMils=DB.milestones.length;
var pmtReviewed=DB.milestones.filter(function(m){return m.pmtStatus&&(m.pmtStatus==='endorsed'||m.pmtStatus==='escalated');}).length;
var pscApproved=DB.milestones.filter(function(m){return m.pscStatus==='approved';}).length;
var pscPending=DB.milestones.filter(function(m){return m.pscStatus==='pending';}).length;
var pmtMembers=pmt.members||[];
var pscMembers=psc.members||[];
var pmtAvatars=pmtMembers.slice(0,4).map(function(u){
var bg=['#6366f1','#0ea5e9','#10b981','#f59e0b'][pmtMembers.indexOf(u)%4];
return '<div class="cm-avatar" style="background:'+bg+'" title="'+u+'">'+u.charAt(0).toUpperCase()+'</div>';
}).join('');
if(pmtMembers.length>4)pmtAvatars+='<div class="cm-avatar cm-more">+' +(pmtMembers.length-4)+'</div>';
var pscAvatars=pscMembers.slice(0,4).map(function(u){
var bg=['#8b5cf6','#ec4899','#f97316','#14b8a6'][pscMembers.indexOf(u)%4];
return '<div class="cm-avatar" style="background:'+bg+'" title="'+u+'">'+u.charAt(0).toUpperCase()+'</div>';
}).join('');
if(pscMembers.length>4)pscAvatars+='<div class="cm-avatar cm-more">+' +(pscMembers.length-4)+'</div>';
return '<div class="committee-banner">'
// PMT Card
+'<div class="committee-card pmt" onclick="showPage(&quot;committees&quot;)" style="cursor:pointer">'
+'<div class="committee-hdr">'
+'<div class="committee-icon"><i class="fas fa-users-gear"></i></div>'
+'<div><div class="committee-name">'+pmt.abbr+' — '+pmt.name+'</div>'
+'<div class="committee-role">'+pmt.chair+' &nbsp;·&nbsp; Meets '+pmt.meetingCadence+'</div>'
+'</div></div>'
+'<div class="committee-stats">'
+'<div class="committee-stat"><div class="committee-stat-val">'+pmtReviewed+'</div><div class="committee-stat-lbl">Reviewed</div></div>'
+'<div class="committee-stat"><div class="committee-stat-val">'+(totalMils-pmtReviewed)+'</div><div class="committee-stat-lbl">Pending</div></div>'
+'<div class="committee-stat"><div class="committee-stat-val">'+DB.activities.filter(function(a){return a.status==='in_progress';}).length+'</div><div class="committee-stat-lbl">Active</div></div>'
+'</div>'
+(pmtAvatars?'<div class="committee-members">'+pmtAvatars+'</div>':'')
+'</div>'
// PSC Card
+'<div class="committee-card psc" onclick="showPage(&quot;committees&quot;)" style="cursor:pointer">'
+'<div class="committee-hdr">'
+'<div class="committee-icon"><i class="fas fa-people-roof"></i></div>'
+'<div><div class="committee-name">'+psc.abbr+' — '+psc.name+'</div>'
+'<div class="committee-role">'+psc.chair+' &nbsp;·&nbsp; Meets '+psc.meetingCadence+'</div>'
+'</div></div>'
+'<div class="committee-stats">'
+'<div class="committee-stat"><div class="committee-stat-val">'+pscApproved+'</div><div class="committee-stat-lbl">Approved</div></div>'
+'<div class="committee-stat"><div class="committee-stat-val">'+pscPending+'</div><div class="committee-stat-lbl">Pending</div></div>'
+'<div class="committee-stat"><div class="committee-stat-val">'+DB.decisions.filter(function(d){return d.status==='pending';}).length+'</div><div class="committee-stat-lbl">Decisions</div></div>'
+'</div>'
+(pscAvatars?'<div class="committee-members">'+pscAvatars+'</div>':'')
+'</div>'
+'</div>';
}
function calcHealthScore(){
var total=DB.activities.length||1;
var done=DB.activities.filter(function(a){return a.status==='completed';}).length;
var td=today();
var overdue=DB.activities.filter(function(a){return a.endDate&&a.endDate<td&&a.status!=='completed';}).length;
var openRisksHigh=DB.risks.filter(function(r){return r.status==='open'&&(r.severity==='high'||r.severity==='critical');}).length;
var pendDec=DB.decisions.filter(function(d){return d.status==='pending';}).length;
var pscApproved=DB.milestones.filter(function(m){return m.pscStatus==='approved';}).length;
var totalMils=DB.milestones.length||1;
var milPct=Math.round(done/total*100);
var schedScore=Math.max(0,100-overdue*8);
var riskScore=Math.max(0,100-openRisksHigh*15);
var decScore=Math.max(0,100-pendDec*10);
var pscScore=Math.round(pscApproved/totalMils*100);
var composite=Math.round(milPct*0.30+schedScore*0.25+decScore*0.20+riskScore*0.15+pscScore*0.10);
return{score:composite,milPct:milPct,schedScore:schedScore,riskScore:riskScore,decScore:decScore,pscScore:pscScore,
rag:composite>=80?'green':composite>=60?'amber':'red',
ragLabel:composite>=80?'On Track':composite>=60?'Needs Attention':'At Risk'};
}
function renderDecisionsRequired(){
var td=today();
var monthEnd=today().slice(0,7)+'-'+(new Date(new Date().getFullYear(),new Date().getMonth()+1,0).getDate());
// All pending decisions
var pending=DB.decisions.filter(function(d){return d.status==='pending';});
// Sort by days open descending
pending.sort(function(a,b){
var da=a.dateRaised?Math.round((new Date(td)-new Date(a.dateRaised))/(1000*60*60*24)):0;
var db2=b.dateRaised?Math.round((new Date(td)-new Date(b.dateRaised))/(1000*60*60*24)):0;
return db2-da;
});
if(!pending.length){
return '<div class="dec-required-panel">'
+'<div class="dec-required-hdr"><div class="dec-required-title"><i class="fas fa-circle-check"></i> Decisions Required This Month</div></div>'
+'<div style="padding:24px;text-align:center;color:var(--g400)"><i class="fas fa-check-circle" style="font-size:28px;color:#22c55e;display:block;margin-bottom:8px"></i>No pending decisions — all clear!</div>'
+'</div>';
}
var rows=pending.map(function(d){
var days=d.dateRaised?Math.round((new Date(td)-new Date(d.dateRaised))/(1000*60*60*24)):0;
var urgColor=days>21?'#fee2e2':days>14?'#fff7ed':'#fef9c3';
var urgTxt=days>21?'#991b1b':days>14?'#9a3412':'#854d0e';
var urgLabel=days>21?'URGENT':days>14?'HIGH':'REVIEW';
var committee=d.committee||'PSC';
return '<div class="dec-item">'
+'<div class="dec-urgency" style="background:'+urgColor+';color:'+urgTxt+'">'
+'<span>'+days+'</span><span class="dec-urgency-lbl">days</span>'
+'</div>'
+'<div class="dec-body">'
+'<div class="dec-title-text">'+d.title+'</div>'
+'<div class="dec-meta">'
+'<span style="background:#ede9fe;color:#6d28d9;font-size:9.5px;font-weight:700;padding:1px 6px;border-radius:6px;margin-right:6px">'+committee+'</span>'
+'Owner: '+(d.owner||'Unassigned')
+(d.dateRaised?' &nbsp;·&nbsp; Raised: '+d.dateRaised:'')
+'</div>'
+(d.consequence?'<div class="dec-consequence"><i class="fas fa-exclamation-triangle" style="font-size:9px"></i> Consequence of delay: '+d.consequence+'</div>':'')
+'</div>'
+(isEditor()?'<div class="dec-action-btn"><button class="btn btn-primary btn-sm" onclick="openDecisionModal('+d.id+')"><i class="fas fa-gavel"></i> Decide</button></div>':'')
+'</div>';
}).join('');
return '<div class="dec-required-panel">'
+'<div class="dec-required-hdr">'
+'<div class="dec-required-title"><i class="fas fa-gavel"></i> Decisions Required This Month</div>'
+'<span style="font-size:10px;color:rgba(255,255,255,.5);font-weight:600">'+pending.length+' pending · PSC authority</span>'
+'</div>'
+rows
+'</div>';
}
function renderExecutive(){
if(!DB.committees)DB.committees={pmt:{name:'Project Management Team',abbr:'PMT',chair:'',meetingCadence:'Weekly',members:[],responsibilities:[]},psc:{name:'Project Steering Committee',abbr:'PSC',chair:'',meetingCadence:'Monthly',members:[],responsibilities:[]}};
if(!DB.projectNarrative)DB.projectNarrative={thisWeek:'',nextWeek:'',keyMessage:'',lastUpdated:'',updatedBy:''};
var h=calcHealthScore();
var scoreColor=h.rag==='green'?'#22c55e':h.rag==='amber'?'#f59e0b':'#ef4444';
var td=today();
var totalMils=DB.milestones.length;
var doneMils=DB.milestones.filter(function(m){return m.status==='completed';}).length;
var openRisks=DB.risks.filter(function(r){return r.status==='open';}).length;
var pendDec=DB.decisions.filter(function(d){return d.status==='pending';}).length;
var overdue=DB.activities.filter(function(a){return a.endDate&&a.endDate<td&&a.status!=='completed';}).length;
var in30=addDays(td,30);
var _ps=DB.projectStart?new Date(DB.projectStart):new Date();
var upcomingMils=DB.milestones.filter(function(m){
var r=resolveMilDate(m,_ps);
return r.ed&&r.ed>=new Date(td)&&r.ed<=new Date(in30)&&m.status!=='completed';
}).length;
var pmt=DB.committees.pmt, psc=DB.committees.psc;
var lastMeeting=DB.meetings&&DB.meetings.length?DB.meetings[DB.meetings.length-1]:null;
var n=DB.projectNarrative;
g('mainContent').innerHTML=
// Hero scorecard
'<div class="exec-hero">'
+'<div class="exec-hero-top">'
+'<div>'
+'<div class="exec-hero-title">Pinnacle Decision Portal</div>'
+'<div class="exec-hero-name">Project Executive Summary</div>'
+'<div class="exec-hero-sub">As of '+new Date().toLocaleDateString('default',{weekday:'long',day:'numeric',month:'long',year:'numeric'})+'</div>'
+'<div style="display:flex;align-items:center;gap:8px;margin-top:12px;flex-wrap:wrap">'
+'<span><span class="rag-dot rag-'+h.rag+'"></span><span style="font-size:12px;color:rgba(255,255,255,.8);font-weight:700">'+h.ragLabel+'</span></span>'
+'<span style="color:rgba(255,255,255,.2)">|</span>'
+'<span style="font-size:11px;color:rgba(255,255,255,.5)">PMT Chair: '+(pmt.chair||'—')+'</span>'
+'<span style="color:rgba(255,255,255,.2)">|</span>'
+'<span style="font-size:11px;color:rgba(255,255,255,.5)">PSC Chair: '+(psc.chair||'—')+'</span>'
+'</div>'
+'</div>'
+'<div class="health-ring-wrap">'
+'<div class="health-score-big" style="color:'+scoreColor+'">'+h.score+'</div>'
+'<div class="health-score-lbl">Health Score</div>'
+'<div class="health-trend" style="color:'+scoreColor+'">/ 100</div>'
+'</div>'
+'</div>'
+'<div class="exec-kpis">'
+'<div class="exec-kpi"><div class="exec-kpi-val">'+doneMils+'/'+totalMils+'</div><div class="exec-kpi-lbl">Milestones Done</div></div>'
+'<div class="exec-kpi"><div class="exec-kpi-val" style="color:'+(overdue>0?'#f43f5e':'#22c55e')+'">'+overdue+'</div><div class="exec-kpi-lbl">Overdue Items</div></div>'
+'<div class="exec-kpi"><div class="exec-kpi-val" style="color:'+(pendDec>0?'#f59e0b':'#22c55e')+'">'+pendDec+'</div><div class="exec-kpi-lbl">Pending Decisions</div></div>'
+'<div class="exec-kpi"><div class="exec-kpi-val" style="color:'+(openRisks>2?'#f43f5e':openRisks>0?'#f59e0b':'#22c55e')+'">'+openRisks+'</div><div class="exec-kpi-lbl">Open Risks</div></div>'
+'</div>'
+'</div>'
// Key message / narrative
+'<div class="narrative-box">'
+'<div class="nb-hdr">'
+'<div class="nb-title"><i class="fas fa-pen-to-square tgold"></i> Management Commentary</div>'
+(isEditor()?'<button class="btn btn-outline btn-sm" onclick="editNarrative()"><i class="fas fa-pen"></i> Update</button>':'')
+'</div>'
+(n.keyMessage
?'<div class="narrative-text">'+n.keyMessage+'</div>'
+(n.lastUpdated?'<div style="font-size:10px;color:var(--g400);margin-top:8px">Last updated: '+n.lastUpdated+' by '+(n.updatedBy||'—')+'</div>':'')
:'<div class="narrative-text" style="color:var(--g300);font-style:italic">No message entered yet. '+(isEditor()?'Click Update to add the Management Commentary.':'Contact your Project Manager to add a board message.')+'</div>')
+'</div>'
// PM Weekly summary
+'<div class="card"><div class="card-hdr"><span class="card-title"><i class="fas fa-calendar-week tgold"></i> This Week / Next Week</span>'
+(isEditor()?'<button class="btn btn-outline btn-sm" onclick="editNarrative()"><i class="fas fa-pen"></i> Update</button>':'')
+'</div><div class="card-body"><div class="fg-2">'
+'<div class="fg"><div style="font-size:10px;font-weight:800;color:var(--navy);letter-spacing:.5px;text-transform:uppercase;margin-bottom:6px">This Week</div>'
+'<div style="font-size:13px;color:var(--g600);line-height:1.7;min-height:50px">'+(n.thisWeek||'<span style="color:var(--g300);font-style:italic">Not yet updated</span>')+'</div></div>'
+'<div class="fg"><div style="font-size:10px;font-weight:800;color:var(--navy);letter-spacing:.5px;text-transform:uppercase;margin-bottom:6px">Next Week</div>'
+'<div style="font-size:13px;color:var(--g600);line-height:1.7;min-height:50px">'+(n.nextWeek||'<span style="color:var(--g300);font-style:italic">Not yet updated</span>')+'</div></div>'
+'</div></div></div>'
// Decisions Required
+renderDecisionsRequired()
// Upcoming milestones
+'<div class="card"><div class="card-hdr"><span class="card-title"><i class="fas fa-flag-checkered tgold"></i> Milestones Due in Next 30 Days</span>'
+'<button class="btn btn-outline btn-sm" onclick="showPage(&quot;phases&quot;)"><i class="fas fa-layer-group"></i> Full View</button>'
+'</div><div class="card-body" style="padding:0">'
+(function(){
var td2=today();
var in30=addDays(td2,30);
var projStart=DB.projectStart?new Date(DB.projectStart):new Date();
projStart=new Date(projStart.getFullYear(),projStart.getMonth(),1);
function localResolve(m){return resolveMilDate(m,null);}
var mils=DB.milestones.filter(function(m){
var r=localResolve(m);
return r.ed&&r.ed>=new Date(td2)&&r.ed<=new Date(in30)&&m.status!=='completed';
}).sort(function(a,b){
var ra=localResolve(a),rb=localResolve(b);
return (ra.ed||0)-(rb.ed||0);
});
if(!mils.length)return '<div style="padding:20px;text-align:center;color:var(--g400)">No milestones due in the next 30 days.</div>';
return mils.map(function(m){
var ph=DB.phases.find(function(p){return p.id===m.phaseId;})||{num:'?'};
var r=localResolve(m);
var daysLeft=r.ed?Math.round((r.ed-new Date(td2))/(1000*60*60*24)):'-';
var pscBadge=m.pscStatus==='approved'?'<span style="background:#dcfce7;color:#15803d;font-size:9px;font-weight:700;padding:1px 6px;border-radius:6px;margin-left:6px">PSC Approved</span>':m.pscStatus==='pending'?'<span style="background:#fee2e2;color:#991b1b;font-size:9px;font-weight:700;padding:1px 6px;border-radius:6px;margin-left:6px">PSC Pending</span>':'';
return '<div style="display:flex;align-items:center;gap:12px;padding:10px 18px;border-bottom:1px solid var(--g100)">'
+'<div style="width:40px;height:40px;border-radius:9px;background:var(--g50);border:1px solid var(--g200);display:flex;flex-direction:column;align-items:center;justify-content:center;flex-shrink:0">'
+'<span style="font-size:16px;font-weight:900;color:var(--navy);line-height:1">'+daysLeft+'</span>'
+'<span style="font-size:8px;color:var(--g400);font-weight:700">days</span>'
+'</div>'
+'<div style="flex:1;min-width:0">'
+'<div style="font-size:13px;font-weight:700;color:var(--navy);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+m.title+pscBadge+'</div>'
+'<div style="font-size:11px;color:var(--g400);margin-top:2px">Phase '+ph.num+(r.ed?' · Due: '+r.ed.toLocaleDateString('default',{day:'numeric',month:'short',year:'numeric'}):'')+'</div>'
+'</div>'
+'<div><span style="font-size:10px;font-weight:700;padding:3px 8px;border-radius:6px;background:var(--g100);color:var(--g600)">'+milestonePct(m.id)+'%</span></div>'
+'</div>';
}).join('');
})()
+'</div></div>'
// SPI mini widget on executive page
+(function(){
var s=calcSPI();
var spiCol=s.spi>=0.95?'#10b981':s.spi>=0.80?'#f59e0b':s.spi>=0.65?'#f97316':'#f43f5e';
return '<div class="card" style="margin-bottom:16px"><div class="card-hdr">'
+'<span class="card-title"><i class="fas fa-gauge-high tgold"></i> Schedule Performance</span>'
+'<button class="btn btn-outline btn-sm" onclick="showPage(&quot;schedule&quot;)"><i class="fas fa-chart-line"></i> Full View</button>'
+'</div><div class="card-body"><div style="display:flex;gap:20px;flex-wrap:wrap;align-items:center">'
+'<div style="text-align:center;min-width:80px">'
+'<div style="font-size:36px;font-weight:900;font-family:Playfair Display,serif;color:'+spiCol+';line-height:1">'+s.spi.toFixed(2)+'</div>'
+'<div style="font-size:10px;font-weight:800;color:var(--g400);letter-spacing:.8px;text-transform:uppercase;margin-top:3px">SPI</div>'
+'</div>'
+'<div style="flex:1;min-width:200px">'
+'<div style="font-size:13px;font-weight:700;color:var(--navy);margin-bottom:4px">'+s.statusLabel+'</div>'
+'<div style="font-size:12px;color:var(--g500)">Projected completion: <b style="color:var(--navy)">'+s.projEnd2.toLocaleDateString('default',{month:'long',year:'numeric'})+'</b></div>'
+'<div style="font-size:12px;color:var(--g500);margin-top:2px">'+s.elapsedPct+'% of timeline elapsed &nbsp;·&nbsp; '+s.completePct+'% of milestones complete</div>'
+(s.daysVariance>0?'<div style="font-size:11px;color:#b91c1c;font-weight:600;margin-top:4px"><i class="fas fa-clock"></i> '+s.daysVariance+' days behind schedule</div>':'<div style="font-size:11px;color:#15803d;font-weight:600;margin-top:4px"><i class="fas fa-check-circle"></i> On track</div>')
+'</div>'
+'<button class="btn btn-outline btn-sm" onclick="showPage(&quot;stakeholders&quot;)" style="flex-shrink:0"><i class="fas fa-handshake"></i> Stakeholders</button>'
+'</div></div></div>';
})()
// Score breakdown
+'<div class="card"><div class="card-hdr"><span class="card-title"><i class="fas fa-chart-bar tgold"></i> Health Score Breakdown</span></div><div class="card-body">'
+[
{label:'Milestone Completion',val:h.milPct,weight:'30%',col:'#6366f1'},
{label:'Schedule Adherence',val:h.schedScore,weight:'25%',col:'#0ea5e9'},
{label:'Decision Velocity',val:h.decScore,weight:'20%',col:'#10b981'},
{label:'Risk Control',val:h.riskScore,weight:'15%',col:'#f59e0b'},
{label:'PSC Approval Rate',val:h.pscScore,weight:'10%',col:'#ec4899'},
].map(function(s){
return '<div style="margin-bottom:12px">'
+'<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px">'
+'<span style="font-weight:600;color:var(--navy)">'+s.label+'</span>'
+'<span style="color:var(--g400);font-size:11px">Weight: '+s.weight+' &nbsp;<b style="color:var(--navy)">'+s.val+'</b></span>'
+'</div>'
+'<div style="height:6px;background:var(--g100);border-radius:3px">'
+'<div style="height:100%;width:'+s.val+'%;background:'+s.col+';border-radius:3px;transition:width .6s ease"></div>'
+'</div></div>';
}).join('')
+'</div></div>';
}
function editNarrative(){
if(!DB.projectNarrative)DB.projectNarrative={thisWeek:'',nextWeek:'',keyMessage:'',lastUpdated:'',updatedBy:''};
var n=DB.projectNarrative;
var m=document.createElement('div');
m.className='overlay open';
m.innerHTML='<div class="modal" style="max-width:580px">'
+'<div class="modal-hdr"><span class="modal-title"><i class="fas fa-pen-to-square tgold"></i> Update Management Commentary</span>'
+'<button class="modal-close" onclick="this.closest(&quot;.overlay&quot;).remove()">&#x2715;</button></div>'
+'<div class="modal-body">'
+'<div class="fg"><label>Management Commentary</label>'
+'<textarea id="pmKeyMsg" rows="3" placeholder="Provide a concise management commentary on project status, key achievements, risks, and decisions required for the board...">'+n.keyMessage+'</textarea></div>'
+'<div class="fg-2">'
+'<div class="fg"><label>This Week — Achievements</label><textarea id="pmThisWeek" rows="3" placeholder="What was accomplished this week...">'+n.thisWeek+'</textarea></div>'
+'<div class="fg"><label>Next Week — Planned</label><textarea id="pmNextWeek" rows="3" placeholder="What is planned for next week...">'+n.nextWeek+'</textarea></div>'
+'</div>'
+'</div>'
+'<div class="modal-foot">'
+'<button class="btn btn-outline" onclick="this.closest(&quot;.overlay&quot;).remove()">Cancel</button>'
+'<button class="btn btn-primary" onclick="saveNarrative()"><i class="fas fa-save"></i> Save Narrative</button>'
+'</div></div>';
document.body.appendChild(m);
m.addEventListener('click',function(e){if(e.target===m)m.remove();});
}
function saveNarrative(){
if(!DB.projectNarrative)DB.projectNarrative={};
DB.projectNarrative.keyMessage=(document.getElementById('pmKeyMsg')||{}).value||'';
DB.projectNarrative.thisWeek=(document.getElementById('pmThisWeek')||{}).value||'';
DB.projectNarrative.nextWeek=(document.getElementById('pmNextWeek')||{}).value||'';
DB.projectNarrative.lastUpdated=today();
DB.projectNarrative.updatedBy=CU?CU.fullName||CU.username:'';
saveDB(true,'Management Commentary updated by '+(CU?CU.fullName:''));
document.querySelector('.overlay.open').remove();
flash('Management Commentary updated successfully.');
renderExecutive();
}
function renderMeetings(){
if(!DB.meetings)DB.meetings=[];
var pmt=DB.committees&&DB.committees.pmt?DB.committees.pmt:{name:'Project Management Team',abbr:'PMT'};
var psc=DB.committees&&DB.committees.psc?DB.committees.psc:{name:'Project Steering Committee',abbr:'PSC'};
var meetingRows=DB.meetings.length?DB.meetings.slice().reverse().map(function(mt,i){
var agendaRows=(mt.agenda||[]).map(function(ag,j){
var stClass={agreed:'as-agreed',noted:'as-noted',deferred:'as-deferred',pending:'as-pending'}[ag.status]||'as-pending';
var stLabel={agreed:'Agreed',noted:'Noted',deferred:'Deferred',pending:'Pending'}[ag.status]||'Pending';
return '<div class="agenda-item">'
+'<div class="agenda-num">'+(j+1)+'</div>'
+'<div class="agenda-text">'+ag.item+'</div>'
+'<span class="agenda-status '+stClass+'">'+stLabel+'</span>'
+'</div>';
}).join('');
var decBox=mt.finalDecision?'<div class="meeting-decision-box"><h5><i class="fas fa-gavel"></i> Final Decision / Resolution</h5><p>'+mt.finalDecision+'</p></div>':'';
var typeClass=mt.type==='PSC'?'mtb-psc':'mtb-pmt';
return '<div class="meeting-card">'
+'<div class="meeting-card-hdr" onclick="this.nextElementSibling.classList.toggle(&quot;open&quot;)">'
+'<div style="display:flex;align-items:center;gap:10px;min-width:0">'
+'<span class="meeting-type-badge '+typeClass+'">'+mt.type+'</span>'
+'<div style="min-width:0">'
+'<div style="font-size:13px;font-weight:700;color:var(--navy);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+mt.title+'</div>'
+'<div style="font-size:11px;color:var(--g400)">'+mt.date+(mt.chair?' &nbsp;·&nbsp; Chaired by: '+mt.chair:'')+(mt.attendees?' &nbsp;·&nbsp; '+mt.attendees+' attendees':'')+'</div>'
+'</div></div>'
+'<div style="display:flex;align-items:center;gap:8px;flex-shrink:0">'
+(isAdmin()?'<button class="btn btn-outline btn-xs" onclick="event.stopPropagation();editMeeting('+mt.id+')"><i class="fas fa-pen"></i></button>':'')
+'<i class="fas fa-chevron-down" style="font-size:11px;color:var(--g400);transition:transform .2s"></i>'
+'</div>'
+'</div>'
+'<div class="meeting-body">'
+(mt.notes?'<div style="font-size:12.5px;color:var(--g600);padding:10px 0 6px;line-height:1.6;border-bottom:1px solid var(--g100)">'+mt.notes+'</div>':'')
+(agendaRows?'<div style="margin-top:10px;font-size:10px;font-weight:800;color:var(--g400);letter-spacing:.8px;text-transform:uppercase;margin-bottom:6px">Agenda Items</div>'+agendaRows:'')
+decBox
+'</div>'
+'</div>';
}).join('')
:'<div style="padding:32px;text-align:center;color:var(--g400)"><i class="fas fa-calendar-plus" style="font-size:32px;display:block;margin-bottom:10px;opacity:.3"></i>No meetings recorded yet. Click Add Meeting to log your first PMT or PSC meeting.</div>';
g('mainContent').innerHTML=
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap;gap:8px">'
+'<div style="display:flex;gap:8px">'
+'<div style="background:#dbeafe;color:#1d4ed8;font-size:11px;font-weight:700;padding:4px 12px;border-radius:20px"><i class="fas fa-users-gear"></i> PMT: '+DB.meetings.filter(function(m){return m.type==='PMT';}).length+' meetings</div>'
+'<div style="background:#ede9fe;color:#6d28d9;font-size:11px;font-weight:700;padding:4px 12px;border-radius:20px"><i class="fas fa-people-roof"></i> PSC: '+DB.meetings.filter(function(m){return m.type==='PSC';}).length+' meetings</div>'
+'</div>'
+(isEditor()?'<button class="btn btn-primary" onclick="openAddMeeting()"><i class="fas fa-plus"></i> Add Meeting</button>':'')
+'</div>'
+meetingRows;
}
function openAddMeeting(editId){
var mt=editId?DB.meetings.find(function(x){return x.id===editId;}):null;
var agendaLines=mt&&mt.agenda?mt.agenda.map(function(a){return a.item;}).join('\n'):'';
var m=document.createElement('div');
m.className='overlay open';
m.innerHTML='<div class="modal" style="max-width:600px">'
+'<div class="modal-hdr"><span class="modal-title"><i class="fas fa-calendar-plus tgold"></i> '+(mt?'Edit':'Add')+' Meeting</span>'
+'<button class="modal-close" onclick="this.closest(&quot;.overlay&quot;).remove()">&#x2715;</button></div>'
+'<div class="modal-body">'
+'<div class="fg-2">'
+'<div class="fg"><label>Committee</label><select id="mtType"><option'+(mt&&mt.type==='PMT'?' selected':'')+'>PMT</option><option'+(mt&&mt.type==='PSC'?' selected':'')+'>PSC</option></select></div>'
+'<div class="fg"><label>Meeting Date</label><input type="date" id="mtDate" value="'+(mt?mt.date:today())+'"></div>'
+'</div>'
+'<div class="fg"><label>Meeting Title</label><input type="text" id="mtTitle" placeholder="e.g. PMT Weekly #12 / PSC Q2 Review" value="'+(mt?mt.title:'')+'"></div>'
+'<div class="fg-2">'
+'<div class="fg"><label>Chaired By</label><input type="text" id="mtChair" placeholder="Name of chair" value="'+(mt?mt.chair||'':'')+'"></div>'
+'<div class="fg"><label>No. of Attendees</label><input type="number" id="mtAttendees" min="1" value="'+(mt?mt.attendees||'':'')+'"></div>'
+'</div>'
+'<div class="fg"><label>Meeting Notes / Context</label><textarea id="mtNotes" rows="2" placeholder="Brief context or summary...">'+((mt&&mt.notes)||'')+'</textarea></div>'
+'<div class="fg"><label>Agenda Items (one per line)</label><textarea id="mtAgenda" rows="5" placeholder="Item 1: Project status update | Item 2: Risk review | Item 3: Budget approval...">'+agendaLines+'</textarea></div>'
+'<div class="fg"><label>Final Decision / Resolution</label><textarea id="mtDecision" rows="2" placeholder="Summary of key decisions made in this meeting...">'+((mt&&mt.finalDecision)||'')+'</textarea></div>'
+'</div>'
+'<div class="modal-foot">'
+'<button class="btn btn-outline" onclick="this.closest(&quot;.overlay&quot;).remove()">Cancel</button>'
+'<button class="btn btn-primary" onclick="saveMeeting('+(mt?mt.id:'null')+')"><i class="fas fa-save"></i> Save Meeting</button>'
+'</div></div>';
document.body.appendChild(m);
m.addEventListener('click',function(e){if(e.target===m)m.remove();});
}
function editMeeting(id){openAddMeeting(id);}
function saveMeeting(editId){
if(!DB.meetings)DB.meetings=[];
if(!DB.ids.meeting)DB.ids.meeting=1;
var type=(document.getElementById('mtType')||{}).value||'PMT';
var date=(document.getElementById('mtDate')||{}).value||today();
var title=(document.getElementById('mtTitle')||{}).value||'Meeting';
var chair=(document.getElementById('mtChair')||{}).value||'';
var attendees=parseInt((document.getElementById('mtAttendees')||{}).value)||0;
var notes=(document.getElementById('mtNotes')||{}).value||'';
var agendaRaw=(document.getElementById('mtAgenda')||{}).value||'';
var decision=(document.getElementById('mtDecision')||{}).value||'';
var agendaItems=agendaRaw.split('\n').filter(function(s){return s.trim();}).map(function(s){
return{item:s.trim(),status:'pending'};
});
if(editId&&editId!=='null'){
var ex=DB.meetings.find(function(x){return x.id===editId;});
if(ex){
ex.type=type;ex.date=date;ex.title=title;ex.chair=chair;ex.attendees=attendees;
ex.notes=notes;ex.agenda=agendaItems;ex.finalDecision=decision;
}
} else {
DB.meetings.push({id:DB.ids.meeting++,type:type,date:date,title:title,chair:chair,
attendees:attendees,notes:notes,agenda:agendaItems,finalDecision:decision,createdBy:CU?CU.username:''});
}
saveDB(true,type+' meeting "'+title+'" recorded by '+(CU?CU.fullName:''));
document.querySelector('.overlay.open').remove();
flash(type+' meeting saved successfully.');
renderMeetings();
}
/* ── SCHEDULE PERFORMANCE INDEX & PROJECTED COMPLETION ── */
function calcSPI(){
var ps=DB.projectStart?new Date(DB.projectStart):new Date();
ps=new Date(ps.getFullYear(),ps.getMonth(),1);
var durationMonths=DB.projectDuration||12;
var projEnd=new Date(ps.getFullYear(),ps.getMonth()+durationMonths,0);
var now=new Date();
var totalMs=projEnd-ps;
var elapsedMs=Math.max(0,Math.min(totalMs,now-ps));
var elapsedPct=Math.round(elapsedMs/totalMs*100);
var totalMils=DB.milestones.length||1;
var doneMils=DB.milestones.filter(function(m){return m.status==='completed';}).length;
var completePct=Math.round(doneMils/totalMils*100);
var totalActs=DB.activities.length||1;
var doneActs=DB.activities.filter(function(a){return a.status==='completed';}).length;
var actPct=Math.round(doneActs/totalActs*100);
// SPI = work done % / time elapsed %
var spi=elapsedPct>0?Math.round((completePct/elapsedPct)*100)/100:1;
// Projected completion
var projMonths=spi>0?Math.round(durationMonths/spi):durationMonths*2;
var projEnd2=new Date(ps.getFullYear(),ps.getMonth()+projMonths,0);
var daysVariance=Math.round((projEnd2-projEnd)/(1000*60*60*24));
var monthsRemaining=Math.max(0,Math.round((projEnd2-now)/(1000*60*60*24*30)));
return{
spi:spi,elapsedPct:elapsedPct,completePct:completePct,actPct:actPct,
projEnd:projEnd,projEnd2:projEnd2,daysVariance:daysVariance,
monthsRemaining:monthsRemaining,durationMonths:durationMonths,
projStart:ps,doneMils:doneMils,totalMils:totalMils,
status:spi>=0.95?'on_track':spi>=0.80?'minor_delay':spi>=0.65?'at_risk':'critical',
statusLabel:spi>=0.95?'On Track':spi>=0.80?'Minor Delay':spi>=0.65?'At Risk':'Critical Delay'
};
}
function renderSchedule(){
if(!DB.projectDuration)DB.projectDuration=12;
var s=calcSPI();
var spiColor=s.spi>=0.95?'#10b981':s.spi>=0.80?'#f59e0b':s.spi>=0.65?'#f97316':'#f43f5e';
var spiClass=s.spi>=0.95?'green':s.spi>=0.80?'amber':s.spi>=0.65?'amber':'red';
var varClass=s.daysVariance<=0?'vb-green':s.daysVariance<=60?'vb-amber':'vb-red';
var varIcon=s.daysVariance<=0?'fa-check-circle':s.daysVariance<=60?'fa-exclamation-triangle':'fa-times-circle';
var varMsg=s.daysVariance<=0?'Project is projected to complete on or before the planned end date'
:'Project is projected to be '+s.daysVariance+' days ('+(Math.round(s.daysVariance/30.4))+' months) behind schedule';
// Phase completion breakdown
var phaseRows=DB.phases.map(function(p){
var mils=DB.milestones.filter(function(m){return m.phaseId===p.id;});
var done=mils.filter(function(m){return m.status==='completed';}).length;
var pct=mils.length?Math.round(done/mils.length*100):0;
var acts=DB.activities.filter(function(a){return DB.milestones.some(function(m){return m.phaseId===p.id&&m.id===a.milestoneId;});});
var actsDone=acts.filter(function(a){return a.status==='completed';}).length;
var td=today();
var overdue=acts.filter(function(a){return a.endDate&&a.endDate<td&&a.status!=='completed';}).length;
var col=pct===100?'#10b981':pct>50?'#6366f1':pct>0?'#f59e0b':'#e2e8f0';
return '<div style="margin-bottom:14px">'
+'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:5px">'
+'<div style="font-size:12.5px;font-weight:700;color:var(--navy)">Phase '+p.num+': '+(p.name.length>40?p.name.slice(0,38)+'...':p.name)+'</div>'
+'<div style="display:flex;align-items:center;gap:8px">'
+(overdue?'<span style="font-size:10px;color:#b91c1c;font-weight:700"><i class="fas fa-exclamation-circle"></i> '+overdue+' overdue</span>':'')
+'<span style="font-size:12px;font-weight:900;color:'+col+'">'+pct+'%</span>'
+'</div></div>'
+'<div style="height:8px;background:var(--g100);border-radius:4px;overflow:hidden">'
+'<div style="height:100%;width:'+pct+'%;background:'+col+';border-radius:4px;transition:width .8s ease"></div>'
+'</div>'
+'<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--g400);margin-top:3px">'
+'<span>'+done+'/'+mils.length+' milestones · '+actsDone+'/'+acts.length+' activities</span>'
+'<span>'+(pct===100?'<span style="color:#10b981;font-weight:700">Complete</span>':pct>0?'In Progress':'Not Started')+'</span>'
+'</div>'
+'</div>';
}).join('');
g('mainContent').innerHTML=
// Project settings bar
(isAdmin()?'<div style="background:#fff;border:1px solid var(--g200);border-radius:10px;padding:12px 16px;margin-bottom:16px;display:flex;align-items:center;gap:16px;flex-wrap:wrap">'
+'<div style="font-size:12px;font-weight:700;color:var(--navy)"><i class="fas fa-cog" style="color:var(--gold)"></i> Project Settings</div>'
+'<div style="display:flex;align-items:center;gap:8px;font-size:12px"><label style="color:var(--g500);font-weight:600">Start:</label>'
+'<input type="month" id="spStartIn" value="'+(DB.projectStart?DB.projectStart.slice(0,7):new Date().toISOString().slice(0,7))+'" style="font-size:11px;padding:3px 8px;border:1px solid var(--g200);border-radius:6px" onchange="updateProjStart(this.value)"></div>'
+'<div style="display:flex;align-items:center;gap:8px;font-size:12px"><label style="color:var(--g500);font-weight:600">Duration (months):</label>'
+'<input type="number" id="spDurIn" value="'+(DB.projectDuration||12)+'" min="1" max="120" style="font-size:11px;padding:3px 8px;border:1px solid var(--g200);border-radius:6px;width:70px" onchange="updateProjDuration(this.value)"></div>'
+'<div style="font-size:11px;color:var(--g400)">Planned end: '+s.projEnd.toLocaleDateString('default',{month:'long',year:'numeric'})+'</div>'
+'</div>':'')
// SPI + projected completion cards
+'<div class="spi-wrap">'
// SPI Card
+'<div class="spi-card '+spiClass+'">'
+'<div class="spi-label">Schedule Performance Index (SPI)</div>'
+'<div class="spi-value" style="color:'+spiColor+'">'+s.spi.toFixed(2)+'</div>'
+'<div class="spi-sub">'
+(s.spi>=0.95?'<i class="fas fa-check-circle" style="color:#10b981"></i> Project is on schedule'
:s.spi>=0.80?'<i class="fas fa-exclamation-triangle" style="color:#f59e0b"></i> Minor delay — monitor closely'
:s.spi>=0.65?'<i class="fas fa-exclamation-circle" style="color:#f97316"></i> At risk — corrective action needed'
:'<i class="fas fa-times-circle" style="color:#f43f5e"></i> Critical delay — immediate intervention required')
+'</div>'
+'<div class="spi-bar-wrap">'
+'<div class="spi-bar-labels"><span>0.0</span><span>0.5</span><span style="font-weight:800;color:var(--navy)">1.0 Target</span><span>1.5</span></div>'
+'<div class="spi-bar-track">'
+'<div class="spi-bar-fill" style="width:'+Math.min(100,Math.round(s.spi/1.5*100))+'%;background:'+spiColor+'"></div>'
+'<div class="spi-bar-needle" style="left:'+(100/1.5)+'%;background:#0f172a"></div>'
+'</div>'
+'<div style="font-size:9px;color:var(--g400);margin-top:3px">SPI = Work Completed % ÷ Time Elapsed % &nbsp;|&nbsp; Target ≥ 1.0</div>'
+'</div></div>'
// Projected Completion Card
+'<div class="spi-card blue">'
+'<div class="spi-label">Projected Completion</div>'
+'<div class="spi-value" style="color:#6366f1;font-size:26px">'+s.projEnd2.toLocaleDateString('default',{month:'long',year:'numeric'})+'</div>'
+'<div class="spi-sub" style="margin-top:6px">'
+'<span style="font-weight:700;color:var(--navy)">'+s.monthsRemaining+' months</span> remaining at current pace'
+'</div>'
+'<div class="proj-badges" style="margin-top:10px">'
+'<span class="proj-badge pb-blue"><i class="fas fa-calendar"></i> Planned: '+s.projEnd.toLocaleDateString('default',{month:'short',year:'numeric'})+'</span>'
+'<span class="proj-badge '+(s.daysVariance<=0?'pb-green':s.daysVariance<=60?'pb-amber':'pb-red')+'">'
+(s.daysVariance<=0?'<i class="fas fa-check"></i> On time':'<i class="fas fa-clock"></i> +'+s.daysVariance+' days')
+'</span>'
+'</div></div>'
+'</div>'
// Variance explanation box
+'<div class="variance-box '+varClass+'" style="margin-bottom:16px">'
+'<i class="fas '+varIcon+'" style="font-size:18px;flex-shrink:0"></i>'
+'<div><div style="font-weight:700;font-size:13px">'+s.statusLabel+'</div>'
+'<div style="font-size:12px;margin-top:2px;opacity:.85">'+varMsg+'</div></div>'
+'</div>'
// Visual timeline
+'<div class="proj-timeline">'
+'<div class="proj-tl-hdr">'
+'<div class="proj-tl-title"><i class="fas fa-timeline tgold"></i> Project Timeline</div>'
+'<div style="display:flex;gap:10px;font-size:11px;flex-wrap:wrap">'
+'<span><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#6366f1;margin-right:4px"></span>Time elapsed</span>'
+'<span><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#10b981;margin-right:4px"></span>Work complete</span>'
+'</div></div>'
+'<div class="proj-tl-track">'
+'<div class="proj-tl-elapsed" style="width:'+s.elapsedPct+'%"></div>'
+'<div class="proj-tl-done" style="width:'+s.completePct+'%"></div>'
+'<div class="proj-tl-needle" style="left:'+s.elapsedPct+'%"></div>'
+'</div>'
+'<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--g400);margin-top:4px">'
+'<span>'+s.projStart.toLocaleDateString('default',{month:'short',year:'numeric'})+'</span>'
+'<span>'+s.projEnd.toLocaleDateString('default',{month:'short',year:'numeric'})+'</span>'
+'</div>'
+'<div style="display:flex;gap:16px;margin-top:12px;flex-wrap:wrap">'
+'<div style="font-size:12px"><span style="font-weight:800;color:#6366f1">'+s.elapsedPct+'%</span> <span style="color:var(--g500)">time elapsed</span></div>'
+'<div style="font-size:12px"><span style="font-weight:800;color:#10b981">'+s.completePct+'%</span> <span style="color:var(--g500)">milestones complete</span></div>'
+'<div style="font-size:12px"><span style="font-weight:800;color:#f59e0b">'+s.actPct+'%</span> <span style="color:var(--g500)">activities complete</span></div>'
+'</div></div>'
// Phase breakdown
+'<div class="card"><div class="card-hdr"><span class="card-title"><i class="fas fa-layer-group tgold"></i> Phase-by-Phase Completion</span></div>'
+'<div class="card-body">'+phaseRows+'</div></div>';
}
function updateProjDuration(val){
var v=parseInt(val);
if(!v||v<1)return;
DB.projectDuration=v;
saveDB();
renderSchedule();
}
/* ── STAKEHOLDER ENGAGEMENT TRACKER ── */
function renderStakeholders(){
if(!DB.stakeholders)DB.stakeholders=[];
var filter=window._shFilter||'all';
var types=['board','regulatory','vendor','internal','external'];
var typeLabels={board:'Board',regulatory:'Regulatory',vendor:'Vendor',internal:'Internal',external:'External'};
var typeCols={board:'#7c3aed',regulatory:'#be185d',vendor:'#15803d',internal:'#1d4ed8',external:'#b45309'};
var typeClasses={board:'st-board',regulatory:'st-regulatory',vendor:'st-vendor',internal:'st-internal',external:'st-external'};
var engColors={high:'#22c55e',medium:'#f59e0b',low:'#ef4444',inactive:'#9ca3af'};
function engLabel(level){return level>=75?'High':level>=50?'Medium':level>0?'Low':'Inactive';}
function engStatus(level){return level>=75?'high':level>=50?'medium':level>0?'low':'inactive';}
// Summary tiles
var high=DB.stakeholders.filter(function(s){return s.engagementLevel>=75;}).length;
var med=DB.stakeholders.filter(function(s){return s.engagementLevel>=50&&s.engagementLevel<75;}).length;
var low=DB.stakeholders.filter(function(s){return s.engagementLevel<50&&s.engagementLevel>0;}).length;
var inactive=DB.stakeholders.filter(function(s){return s.engagementLevel===0||s.status==='inactive';}).length;
var filtered=filter==='all'?DB.stakeholders:DB.stakeholders.filter(function(s){return s.type===filter;});
var cards=filtered.map(function(sh){
var col=typeCols[sh.type]||'#64748b';
var tcls=typeClasses[sh.type]||'st-external';
var eLevel=sh.engagementLevel||0;
var eStat=engStatus(eLevel);
var eCol=engColors[eStat];
var initials=sh.name.split(' ').map(function(w){return w[0];}).join('').slice(0,2).toUpperCase();
var phaseNames=(sh.linkedPhases||[]).map(function(pid){
var p=DB.phases.find(function(x){return x.id===pid;});
return p?'Ph.'+p.num:'';
}).filter(Boolean).join(', ');
return '<div class="sh-card">'
+'<div class="sh-card-top">'
+'<div class="sh-avatar" style="background:'+col+'">'+initials+'</div>'
+'<div style="flex:1;min-width:0">'
+'<div class="sh-name">'+sh.name+'</div>'
+'<div class="sh-org">'+sh.org+'</div>'
+'<span class="sh-type '+tcls+'">'+typeLabels[sh.type]+'</span>'
+'</div>'
+(isEditor()?'<button class="btn btn-outline btn-xs" style="flex-shrink:0" onclick="editStakeholder('+sh.id+')"><i class="fas fa-pen"></i></button>':'')
+'</div>'
+'<div class="sh-card-bottom">'
+'<div style="font-size:11px;color:var(--g600);margin-bottom:6px">'+sh.role+'</div>'
+'<div style="font-size:10px;font-weight:700;color:var(--g400);margin-bottom:3px;display:flex;justify-content:space-between">'
+'<span>Engagement Level</span><span style="color:'+eCol+';font-weight:800">'+eLevel+'% — '+engLabel(eLevel)+'</span>'
+'</div>'
+'<div class="sh-engagement-bar">'
+'<div class="sh-engagement-fill" style="width:'+eLevel+'%;background:'+eCol+'"></div>'
+'</div>'
+'<div class="sh-meta">'
+'<span><span class="sh-status-dot eng-'+eStat+'"></span>'+sh.status.charAt(0).toUpperCase()+sh.status.slice(1)+'</span>'
+(phaseNames?'<span style="font-size:10px;color:var(--g400)">'+phaseNames+'</span>':'')
+'</div>'
+(sh.notes?'<div style="font-size:10.5px;color:var(--g500);margin-top:6px;padding-top:6px;border-top:1px solid var(--g100);font-style:italic">'+sh.notes+'</div>':'')
+(sh.owner?'<div style="font-size:10px;color:var(--g400);margin-top:5px"><i class="fas fa-user-tie" style="font-size:9px"></i> Relationship owner: <b>'+sh.owner+'</b></div>':'')
+'</div></div>';
}).join('');
g('mainContent').innerHTML=
'<div class="sh-summary-row">'
+'<div class="sh-sum-tile"><div class="sh-sum-val">'+DB.stakeholders.length+'</div><div class="sh-sum-lbl">Total</div></div>'
+'<div class="sh-sum-tile"><div class="sh-sum-val" style="color:#22c55e">'+high+'</div><div class="sh-sum-lbl">High Engaged</div></div>'
+'<div class="sh-sum-tile"><div class="sh-sum-val" style="color:#f59e0b">'+med+'</div><div class="sh-sum-lbl">Medium</div></div>'
+'<div class="sh-sum-tile"><div class="sh-sum-val" style="color:#ef4444">'+low+'</div><div class="sh-sum-lbl">Low Engaged</div></div>'
+'<div class="sh-sum-tile"><div class="sh-sum-val" style="color:#9ca3af">'+inactive+'</div><div class="sh-sum-lbl">Inactive</div></div>'
+'</div>'
+'<div class="sh-filter-bar">'
+'<span style="font-size:11px;font-weight:700;color:var(--g500)">Filter:</span>'
+'<button class="sh-filter-btn'+(filter==='all'?' active':'')+'" onclick="shFilter(&quot;all&quot;)">All ('+DB.stakeholders.length+')</button>'
+types.map(function(t){
var cnt=DB.stakeholders.filter(function(s){return s.type===t;}).length;
if(!cnt)return '';
return '<button class="sh-filter-btn'+(filter===t?' active':'')+'" onclick="shFilter(&quot;'+t+'&quot;)">'+typeLabels[t]+' ('+cnt+')</button>';
}).join('')
+(isEditor()?'<button class="btn btn-primary btn-sm" style="margin-left:auto" onclick="editStakeholder(null)"><i class="fas fa-plus"></i> Add Stakeholder</button>':'')
+'</div>'
+(cards?'<div class="sh-grid">'+cards+'</div>'
:'<div style="text-align:center;padding:32px;color:var(--g400)"><i class="fas fa-handshake" style="font-size:32px;display:block;margin-bottom:10px;opacity:.3"></i>No stakeholders found.</div>');
}
function shFilter(type){
window._shFilter=type;
renderStakeholders();
}
function editStakeholder(id){
if(!DB.stakeholders)DB.stakeholders=[];
var sh=id?DB.stakeholders.find(function(x){return x.id===id;}):null;
var phOpts=DB.phases.map(function(p){
var sel=sh&&sh.linkedPhases&&sh.linkedPhases.indexOf(p.id)>=0?' selected':'';
return '<option value="'+p.id+'"'+sel+'>Phase '+p.num+': '+p.name.slice(0,30)+'</option>';
}).join('');
var m=document.createElement('div');
m.className='overlay open';
m.innerHTML='<div class="modal" style="max-width:520px">'
+'<div class="modal-hdr"><span class="modal-title"><i class="fas fa-handshake tgold"></i> '+(sh?'Edit':'Add')+' Stakeholder</span>'
+'<button class="modal-close" onclick="this.closest(&quot;.overlay&quot;).remove()">&#x2715;</button></div>'
+'<div class="modal-body">'
+'<div class="fg-2">'
+'<div class="fg"><label>Name / Organisation</label><input type="text" id="shName" value="'+(sh?sh.name:'')+'" placeholder="e.g. Central Bank"></div>'
+'<div class="fg"><label>Organisation / Department</label><input type="text" id="shOrg" value="'+(sh?sh.org:'')+'" placeholder="e.g. State Bank of Pakistan"></div>'
+'</div>'
+'<div class="fg-2">'
+'<div class="fg"><label>Type</label><select id="shType">'
+['board','regulatory','vendor','internal','external'].map(function(t){return '<option value="'+t+'"'+(sh&&sh.type===t?' selected':'')+'>'+t.charAt(0).toUpperCase()+t.slice(1)+'</option>';}).join('')
+'</select></div>'
+'<div class="fg"><label>Status</label><select id="shStatus">'
+['active','monitoring','at_risk','inactive'].map(function(t){return '<option value="'+t+'"'+(sh&&sh.status===t?' selected':'')+'>'+t.replace('_',' ').replace(/\w/g,function(c){return c.toUpperCase();})+'</option>';}).join('')
+'</select></div>'
+'</div>'
+'<div class="fg"><label>Role / Relevance to Project</label><input type="text" id="shRole" value="'+(sh?sh.role:'')+'" placeholder="e.g. Regulatory approval authority"></div>'
+'<div class="fg-2">'
+'<div class="fg"><label>Engagement Level (0100)</label><input type="range" id="shEng" min="0" max="100" value="'+(sh?sh.engagementLevel:50)+'" oninput="document.getElementById(&quot;shEngVal&quot;).textContent=this.value+&quot;%&quot;"><span id="shEngVal">'+(sh?sh.engagementLevel:50)+'%</span></div>'
+'<div class="fg"><label>Relationship Owner</label><input type="text" id="shOwner" value="'+(sh?sh.owner||'':'')+'" placeholder="e.g. Chief Risk Officer"></div>'
+'</div>'
+'<div class="fg"><label>Linked Phases (hold Ctrl to select multiple)</label>'
+'<select id="shPhases" multiple style="height:90px">'+phOpts+'</select></div>'
+'<div class="fg-2">'
+'<div class="fg"><label>Last Contact Date</label><input type="date" id="shLast" value="'+(sh&&sh.lastContact?sh.lastContact:'')+'"></div>'
+'<div class="fg"><label>Next Contact Date</label><input type="date" id="shNext" value="'+(sh&&sh.nextContact?sh.nextContact:'')+'"></div>'
+'</div>'
+'<div class="fg"><label>Notes</label><textarea id="shNotes" rows="2">'+((sh&&sh.notes)||'')+'</textarea></div>'
+'</div>'
+'<div class="modal-foot">'
+(sh&&isAdmin()?'<button class="btn btn-danger btn-sm" onclick="deleteStakeholder('+sh.id+')"><i class="fas fa-trash"></i> Delete</button>':'')
+'<button class="btn btn-outline" onclick="this.closest(&quot;.overlay&quot;).remove()">Cancel</button>'
+'<button class="btn btn-primary" onclick="saveStakeholder('+(sh?sh.id:'null')+')"><i class="fas fa-save"></i> Save</button>'
+'</div></div>';
document.body.appendChild(m);
m.addEventListener('click',function(e){if(e.target===m)m.remove();});
}
function saveStakeholder(editId){
if(!DB.stakeholders)DB.stakeholders=[];
if(!DB.ids.stakeholder)DB.ids.stakeholder=100;
var name=(document.getElementById('shName')||{}).value||'';
var org=(document.getElementById('shOrg')||{}).value||'';
var type=(document.getElementById('shType')||{}).value||'internal';
var status=(document.getElementById('shStatus')||{}).value||'active';
var role=(document.getElementById('shRole')||{}).value||'';
var eng=parseInt((document.getElementById('shEng')||{}).value)||50;
var owner=(document.getElementById('shOwner')||{}).value||'';
var notes=(document.getElementById('shNotes')||{}).value||'';
var last=(document.getElementById('shLast')||{}).value||'';
var next=(document.getElementById('shNext')||{}).value||'';
var phSel=document.getElementById('shPhases');
var phases=phSel?Array.from(phSel.selectedOptions).map(function(o){return parseInt(o.value);}):[];
if(!name){flash('Please enter a stakeholder name.','danger');return;}
if(editId&&editId!=='null'){
var ex=DB.stakeholders.find(function(x){return x.id===editId;});
if(ex){ex.name=name;ex.org=org;ex.type=type;ex.status=status;ex.role=role;ex.engagementLevel=eng;ex.owner=owner;ex.notes=notes;ex.lastContact=last;ex.nextContact=next;ex.linkedPhases=phases;}
} else {
DB.stakeholders.push({id:DB.ids.stakeholder++,name:name,org:org,type:type,status:status,role:role,engagementLevel:eng,owner:owner,notes:notes,lastContact:last,nextContact:next,linkedPhases:phases});
}
saveDB(true,'Stakeholder "'+name+'" '+(editId&&editId!=='null'?'updated':'added')+' by '+(CU?CU.fullName:''));
document.querySelector('.overlay.open').remove();
flash('Stakeholder saved.');
renderStakeholders();
}
function deleteStakeholder(id){
if(!confirm('Delete this stakeholder?'))return;
DB.stakeholders=DB.stakeholders.filter(function(s){return s.id!==id;});
saveDB();
document.querySelector('.overlay.open').remove();
flash('Stakeholder deleted.');
renderStakeholders();
}
function renderCommittees(){
if(!DB.committees)DB.committees={pmt:{name:'Project Management Team',abbr:'PMT',chair:'Project Manager',meetingCadence:'Weekly',members:[],responsibilities:['Activity oversight','Milestone review & endorsement','Risk monitoring & escalation','Progress reporting to PSC','Resource management']},psc:{name:'Project Steering Committee',abbr:'PSC',chair:'Executive Sponsor',meetingCadence:'Monthly',members:[],responsibilities:['Milestone approval','Deliverable sign-off','Budget approval','Strategic decisions','Issue escalation resolution']}};
var pmt=DB.committees.pmt, psc=DB.committees.psc;
// Build approval workflow table rows for milestones
var milRows=DB.milestones.map(function(m){
var ph=DB.phases.find(function(p){return p.id===m.phaseId;})||{num:'?'};
var pmtSt=m.pmtStatus||'not_reviewed';
var pscSt=m.pscStatus||'not_required';
var pmtPill=pmtSt==='endorsed'?'sp-approved':pmtSt==='escalated'||pmtSt==='returned'?'sp-rejected':pmtSt==='under_review'?'sp-pending':'sp-not-required';
var pscPill=pscSt==='approved'?'sp-approved':pscSt==='rejected'?'sp-rejected':pscSt==='pending'?'sp-pending':'sp-not-required';
var pmtLbl=pmtSt.replace(/_/g,' ').replace(/\w/g,function(c){return c.toUpperCase();});
var pscLbl=pscSt.replace(/_/g,' ').replace(/\w/g,function(c){return c.toUpperCase();});
return '<tr>'
+'<td><span style="font-size:10px;background:var(--g100);padding:2px 7px;border-radius:4px;font-weight:700">Ph.'+ph.num+'</span></td>'
+'<td style="font-weight:600;color:var(--navy)">'+m.title+'</td>'
+'<td><span style="font-size:10px;color:var(--g400)">Month '+(m.month||'-')+'</span></td>'
+'<td>'
+'<span class="status-pill '+pmtPill+'">'+pmtLbl+'</span>'
+(isEditor()?'<select onchange="updateMilCommittee('+m.id+',&quot;pmt&quot;,this.value)" style="font-size:10px;margin-left:6px;padding:2px 4px;border:1px solid var(--g200);border-radius:4px">'
+['not_reviewed','under_review','endorsed','escalated','returned'].map(function(s){return '<option value="'+s+'"'+(pmtSt===s?' selected':'')+'>'+s.replace(/_/g,' ').replace(/\b\w/g,function(c){return c.toUpperCase();})+'</option>';}).join('')
+'</select>':'')
+'</td>'
+'<td>'
+'<span class="status-pill '+pscPill+'">'+pscLbl+'</span>'
+(isEditor()?'<select onchange="updateMilCommittee('+m.id+',&quot;psc&quot;,this.value)" style="font-size:10px;margin-left:6px;padding:2px 4px;border:1px solid var(--g200);border-radius:4px">'
+['not_required','pending','approved','rejected'].map(function(s){return '<option value="'+s+'"'+(pscSt===s?' selected':'')+'>'+s.replace(/_/g,' ')+'</option>';}).join('')
+'</select>':'')
+'</td>'
+'</tr>';
}).join('');
// Decision rows awaiting PSC
var decRows=DB.decisions.filter(function(d){return d.status==='pending';}).map(function(d){
return '<tr>'
+'<td style="font-weight:600;color:var(--navy)">'+d.title+'</td>'
+'<td><span class="status-pill sp-pending">Awaiting PSC</span></td>'
+'<td style="font-size:11px;color:var(--g500)">'+(d.owner||'—')+'</td>'
+'<td style="font-size:11px;color:var(--g400)">'+(d.dateRaised||'—')+'</td>'
+(isEditor()?'<td><button class="btn btn-outline btn-xs" onclick="approveDecisionPSC('+d.id+')"><i class="fas fa-check"></i> PSC Approve</button></td>':'<td></td>')
+'</tr>';
}).join('');
// Workflow diagram for a milestone journey
var wfSteps=[
{label:'PMT Review',icon:'fas fa-users-gear',desc:'PMT reviews milestone readiness'},
{label:'PMT Endorsed',icon:'fas fa-thumbs-up',desc:'PMT endorses for PSC'},
{label:'PSC Review',icon:'fas fa-people-roof',desc:'PSC reviews for approval'},
{label:'PSC Approved',icon:'fas fa-circle-check',desc:'Formal approval granted'},
{label:'Implemented',icon:'fas fa-rocket',desc:'Milestone executed'}
];
var wfHTML='<div class="workflow-line">';
wfSteps.forEach(function(s,i){
if(i>0)wfHTML+='<div class="wf-arrow done"></div>';
wfHTML+='<div class="wf-step"><div class="wf-dot done"><i class="'+s.icon+'"></i></div><div class="wf-lbl">'+s.label+'</div></div>';
});
wfHTML+='</div>';
g('mainContent').innerHTML=
// Committee cards
'<div class="committee-banner" style="margin-bottom:20px">'
+'<div class="committee-card pmt" style="cursor:default">'
+'<div class="committee-hdr"><div class="committee-icon"><i class="fas fa-users-gear"></i></div>'
+'<div><div class="committee-name">'+pmt.name+' (PMT)</div>'
+'<div class="committee-role">Chair: <b style="color:rgba(255,255,255,.8)">'+pmt.chair+'</b> &nbsp;·&nbsp; '+pmt.meetingCadence+' meetings</div>'
+'</div></div>'
+'<div style="font-size:11.5px;color:rgba(255,255,255,.6);line-height:1.6;margin-bottom:10px">'+(DB.committees.pmt.role||'')+'</div>'
+'<div style="font-size:10px;color:rgba(255,255,255,.45);font-weight:700;letter-spacing:.5px;text-transform:uppercase;margin-bottom:6px">Responsibilities</div>'
+'<div style="display:flex;flex-wrap:wrap;gap:5px">'
+(pmt.responsibilities||[]).map(function(r){return '<span style="background:rgba(96,165,250,.15);color:#93c5fd;font-size:10px;padding:2px 8px;border-radius:10px;border:1px solid rgba(96,165,250,.2)">'+r+'</span>';}).join('')
+'</div>'
+(isAdmin()?'<div style="margin-top:12px"><button class="btn btn-outline btn-sm" style="border-color:rgba(255,255,255,.2);color:rgba(255,255,255,.7)" onclick="editCommittee(&quot;pmt&quot;)"><i class="fas fa-pen"></i> Edit PMT</button></div>':'')
+'</div>'
+'<div class="committee-card psc" style="cursor:default">'
+'<div class="committee-hdr"><div class="committee-icon"><i class="fas fa-people-roof"></i></div>'
+'<div><div class="committee-name">'+psc.name+' (PSC)</div>'
+'<div class="committee-role">Chair: <b style="color:rgba(255,255,255,.8)">'+psc.chair+'</b> &nbsp;·&nbsp; '+psc.meetingCadence+' meetings</div>'
+'</div></div>'
+'<div style="font-size:11.5px;color:rgba(255,255,255,.6);line-height:1.6;margin-bottom:10px">'+(DB.committees.psc.role||'')+'</div>'
+'<div style="font-size:10px;color:rgba(255,255,255,.45);font-weight:700;letter-spacing:.5px;text-transform:uppercase;margin-bottom:6px">Responsibilities</div>'
+'<div style="display:flex;flex-wrap:wrap;gap:5px">'
+(psc.responsibilities||[]).map(function(r){return '<span style="background:rgba(167,139,250,.15);color:#c4b5fd;font-size:10px;padding:2px 8px;border-radius:10px;border:1px solid rgba(167,139,250,.2)">'+r+'</span>';}).join('')
+'</div>'
+(isAdmin()?'<div style="margin-top:12px"><button class="btn btn-outline btn-sm" style="border-color:rgba(255,255,255,.2);color:rgba(255,255,255,.7)" onclick="editCommittee(&quot;psc&quot;)"><i class="fas fa-pen"></i> Edit PSC</button></div>':'')
+'</div>'
+'</div>'
// Approval workflow
+'<div class="committee-section">'
+'<div class="committee-section-hdr">'
+'<div class="committee-section-title"><i class="fas fa-diagram-project tgold"></i> Milestone Approval Workflow</div>'
+'</div>'
+wfHTML
+'</div>'
// Milestone approval table
+'<div class="committee-section">'
+'<div class="committee-section-hdr">'
+'<div class="committee-section-title"><i class="fas fa-list-check tgold"></i> Milestone Approval Status</div>'
+'<div style="font-size:11px;color:var(--g400)">PMT reviews · PSC approves</div>'
+'</div>'
+'<div class="tbl-wrap"><table class="approval-table"><thead><tr>'
+'<th>Phase</th><th>Milestone</th><th>Timeline</th>'
+'<th><i class="fas fa-users-gear" style="color:#3b82f6"></i> PMT Review</th>'
+'<th><i class="fas fa-people-roof" style="color:#7c3aed"></i> PSC Status</th>'
+'</tr></thead><tbody>'+(milRows||'<tr><td colspan="5" style="text-align:center;padding:20px;color:var(--g400)">No milestones found</td></tr>')+'</tbody></table></div>'
+'</div>'
// Pending PSC decisions
+(decRows?'<div class="committee-section">'
+'<div class="committee-section-hdr">'
+'<div class="committee-section-title"><i class="fas fa-gavel tgold"></i> Decisions Awaiting PSC Approval</div>'
+'</div>'
+'<div class="tbl-wrap"><table class="approval-table"><thead><tr>'
+'<th>Decision</th><th>Status</th><th>Owner</th><th>Raised</th><th>Action</th>'
+'</tr></thead><tbody>'+decRows+'</tbody></table></div>'
+'</div>':'');
}
function updateMilCommittee(milId, committee, status){
var m=DB.milestones.find(function(x){return x.id===milId;});
if(!m)return;
if(committee==='pmt') m.pmtStatus=status;
else m.pscStatus=status;
saveDB(true, committee.toUpperCase()+' updated "'+m.title+'" to '+status);
flash(committee.toUpperCase()+' status updated to "'+status+'" for: '+m.title);
}
function approveDecisionPSC(decId){
var d=DB.decisions.find(function(x){return x.id===decId;});
if(!d)return;
d.status='approved'; d.approvedBy='PSC'; d.approvedDate=today();
saveDB(true,'PSC approved decision: '+d.title);
flash('PSC approval recorded for: '+d.title,'success');
renderCommittees();
}
function editCommittee(type){
var c=DB.committees[type];
if(!c)return;
var col=type==='pmt'?'#3b82f6':'#7c3aed';
var m=document.createElement('div');
m.className='overlay open';
m.innerHTML='<div class="modal" style="max-width:500px">'
+'<div class="modal-hdr" style="border-bottom:2px solid '+col+'20">'
+'<span class="modal-title" style="color:'+col+'"><i class="fas fa-'+(type==='pmt'?'users-gear':'people-roof')+'"></i> Edit '+c.abbr+'</span>'
+'<button class="modal-close" onclick="this.closest(&quot;.overlay&quot;).remove()">&#x2715;</button></div>'
+'<div class="modal-body">'
+'<div class="fg"><label>Committee Name</label><input type="text" id="cName" value="'+c.name+'"></div>'
+'<div class="fg-2">'
+'<div class="fg"><label>Chair / Lead</label><input type="text" id="cChair" value="'+(c.chair||'')+'"></div>'
+'<div class="fg"><label>Meeting Cadence</label><select id="cCadence">'
+['Daily','Weekly','Bi-weekly','Monthly','Quarterly'].map(function(o){return '<option'+(c.meetingCadence===o?' selected':'')+'>'+o+'</option>';}).join('')
+'</select></div></div>'
+'<div class="fg"><label>Role Description</label><textarea id="cRole" rows="2">'+c.role+'</textarea></div>'
+'<div class="fg"><label>Members (comma-separated names)</label><input type="text" id="cMembers" value="'+(c.members||[]).join(', ')+'"></div>'
+'</div>'
+'<div class="modal-foot">'
+'<button class="btn btn-outline" onclick="this.closest(&quot;.overlay&quot;).remove()">Cancel</button>'
+'<button class="btn btn-primary" onclick="saveCommittee(&quot;'+type+'&quot;)"><i class="fas fa-save"></i> Save</button>'
+'</div></div>';
document.body.appendChild(m);
m.addEventListener('click',function(e){if(e.target===m)m.remove();});
}
function saveCommittee(type){
if(!DB.committees)DB.committees={};
if(!DB.committees[type])DB.committees[type]={};
var c=DB.committees[type];
c.name=(document.getElementById('cName')||{}).value||c.name;
c.chair=(document.getElementById('cChair')||{}).value||c.chair;
c.meetingCadence=(document.getElementById('cCadence')||{}).value||c.meetingCadence;
c.role=(document.getElementById('cRole')||{}).value||c.role;
var memStr=(document.getElementById('cMembers')||{}).value||'';
c.members=memStr.split(',').map(function(s){return s.trim();}).filter(Boolean);
saveDB(true, type.toUpperCase()+' committee updated by '+CU.fullName);
document.querySelector('.overlay.open').remove();
flash(type.toUpperCase()+' committee updated successfully.');
renderCommittees();
}
function renderDashboard(){
var acts=DB.activities,total=acts.length,done=acts.filter(function(a){return a.status==='completed';}).length;
var td=today();
var delayed=acts.filter(function(a){return a.endDate&&a.endDate<td&&a.status!=='completed';}).length;
var inWeek=acts.filter(function(a){return a.endDate&&a.endDate>=td&&a.endDate<=addDays(td,7)&&a.status!=='completed';}).length;
var pendD=DB.decisions.filter(function(d){return d.status==='pending';}).length;
var openR=DB.risks.filter(function(r){return r.status==='open';}).length;
var pendA=DB.deliverables.filter(function(d){return d.approvalStatus==='pending';}).length;
var pscPend=DB.milestones.filter(function(m){return m.pscStatus==='pending';}).length;
var overall=pct(total,done);
var phases=DB.phases.slice().sort(function(a,b){return a.order-b.order;});
// Determine active phase: first phase that is not 0% and not 100%
var activePhaseId=null;
for(var _pi=0;_pi<phases.length;_pi++){
var _pp2=phasePct(phases[_pi].id);
if(_pp2>0&&_pp2<100){activePhaseId=phases[_pi].id;break;}
}
// If none in-progress, pick first incomplete
if(activePhaseId===null){
for(var _pi2=0;_pi2<phases.length;_pi2++){
if(phasePct(phases[_pi2].id)<100){activePhaseId=phases[_pi2].id;break;}
}
}
var phCards=phases.map(function(p,i){
var pp=phasePct(p.id);
var mils=DB.milestones.filter(function(m){return m.phaseId===p.id;});
var mDone=mils.filter(function(m){var a=DB.activities.filter(function(x){return x.milestoneId===m.id;});return a.length&&a.every(function(x){return x.status==='completed';});}).length;
var isActive=p.id===activePhaseId;
return '<div class="phase-card'+(isActive?' active-phase':'')+'" style="--pc:'+PC[i%PC.length]+'" onclick="showPage(\'phaseDetail\','+p.id+')">'
+'<div style="font-size:10px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:'+PC[i%PC.length]+';margin-bottom:4px">Phase '+p.num+'</div>'
+'<div style="font-family:\"Playfair Display\",serif;font-size:14px;font-weight:600;color:var(--navy);margin-bottom:11px;line-height:1.3">'+p.name+'</div>'
+'<div class="prog-wrap"><div class="prog-bar" style="width:'+pp+'%;background:'+PC[i%PC.length]+'"></div></div>'
+'<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--g400);margin-top:8px"><span>'+mDone+'/'+mils.length+' milestones</span><span style="font-weight:700;color:'+PC[i%PC.length]+'">'+pp+'%</span></div>'
+'</div>';
}).join('');
var logs=[].concat(DB.logs).reverse().slice(0,12).map(function(l){
var u=DB.users.find(function(x){return x.id===l.userId;});
return '<tr><td class="muted fs12">'+l.timestamp.slice(0,16).replace('T',' ')+'</td>'
+'<td><span class="badge b-navy" style="font-size:10px">'+(u?u.username:'System')+'</span></td>'
+'<td><span class="badge b-gold" style="font-size:10px">'+l.action+'</span></td>'
+'<td class="muted fs12">'+(l.entityName||'—')+'</td></tr>';
}).join('');
g('mainContent').innerHTML=
renderBlockers()
+renderCommitteeBanner()
+'<div class="stat-grid">'
+'<div class="stat-card" style="--sc:#c9a227;--scb:#f5e9c0;cursor:pointer" onclick="showKpiModal(0)"><div class="stat-icon"><i class="fas fa-chart-pie"></i></div><div><div class="stat-val">'+overall+'%</div><div class="stat-lbl">Overall Completion</div></div></div>'
+'<div class="stat-card" style="--sc:#15803d;--scb:#dcfce7;cursor:pointer" onclick="showKpiModal(1)"><div class="stat-icon"><i class="fas fa-check-double"></i></div><div><div class="stat-val">'+done+'</div><div class="stat-lbl">Completed Activities</div></div></div>'
+'<div class="stat-card" style="--sc:#b91c1c;--scb:#fee2e2;cursor:pointer" onclick="showKpiModal(2)"><div class="stat-icon"><i class="fas fa-clock"></i></div><div><div class="stat-val">'+delayed+'</div><div class="stat-lbl">Delayed Tasks</div></div></div>'
+'<div class="stat-card" style="--sc:#d97706;--scb:#fef3c7;cursor:pointer" onclick="showKpiModal(3)"><div class="stat-icon"><i class="fas fa-hourglass-half"></i></div><div><div class="stat-val">'+inWeek+'</div><div class="stat-lbl">Due This Week</div></div></div>'
+'<div class="stat-card" style="--sc:#1d4ed8;--scb:#dbeafe;cursor:pointer" onclick="showKpiModal(4)"><div class="stat-icon"><i class="fas fa-gavel"></i></div><div><div class="stat-val">'+pendD+'</div><div class="stat-lbl">Pending Decisions</div></div></div>'
+'<div class="stat-card" style="--sc:#7c3aed;--scb:#ede9fe;cursor:pointer" onclick="showPage(\'committees\')"><div class="stat-icon"><i class="fas fa-people-roof"></i></div><div><div class="stat-val">'+pscPend+'</div><div class="stat-lbl">PSC Pending</div></div></div>'
+'</div>'
+'<div class="card"><div class="card-hdr"><span class="card-title"><i class="fas fa-bullseye tgold"></i> Project Overall Progress</span><span style="font-size:20px;font-weight:700;color:var(--navy)">'+overall+'%</span></div>'
+'<div class="card-body" style="padding:14px 17px"><div class="prog-wrap" style="height:13px"><div class="prog-bar '+(overall>=75?'ps':overall<30?'pd':'')+'" style="width:'+overall+'%"></div></div>'
+'<div class="flex jb mt-2 fs12 muted"><span>'+done+' of '+total+' activities completed</span><span>'+(total-done)+' remaining</span></div></div></div>'
+'<div class="flex jb ic mb-3 mt-4"><h2 style="font-family:\'Playfair Display\',serif;font-size:16px;color:var(--navy)">Phase Progress</h2><button class="btn btn-outline btn-sm" onclick="showPage(\'phases\')"><i class="fas fa-layer-group"></i> All Phases</button></div>'
+'<div class="phase-grid mb-4">'+(phCards||'<div class="empty" style="grid-column:1/-1"><i class="fas fa-layer-group"></i><p>No phases yet.</p></div>')+'</div>'
+'<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:18px">'
+'<div class="card" style="margin-bottom:0"><div class="card-hdr"><span class="card-title"><i class="fas fa-stream tgold"></i> Project Timeline (Gantt)</span>'
+'<div style="display:flex;align-items:center;gap:8px">'
+'<label style="font-size:11px;color:var(--g400);font-weight:600;white-space:nowrap">Project Start:</label>'
+'<input type="month" id="projStartInput" value="'+(DB.projectStart?DB.projectStart.slice(0,7):new Date().toISOString().slice(0,7))+'" style="font-size:11px;padding:3px 6px;border:1px solid var(--g200);border-radius:5px;color:var(--navy)" onchange="updateProjStart(this.value)">'
+'</div>'
+'</div><div class="card-body" style="padding:0" id="ganttBox"></div></div>'
+'<div class="card" style="margin-bottom:0"><div class="card-hdr"><span class="card-title"><i class="fas fa-chart-pie tgold"></i> Activity Breakdown</span></div><div class="card-body" style="padding:16px 18px"><div style="position:relative;width:100%;height:230px"><canvas id="sch"></canvas><div class="chart-centre-wrap" id="chartCentre"><div class="chart-centre-val" id="chartCentreVal">-</div><div class="chart-centre-lbl" id="chartCentreLbl">TOTAL</div></div></div><div class="stat-tiles" id="actStatTiles" style="margin-top:14px"></div></div></div>'
+'</div>'
+'<div class="card" id="upcomingCard"><div class="card-hdr"><span class="card-title"><i class="fas fa-calendar-days tgold"></i> Upcoming Activities <span style="font-size:11px;font-weight:400;color:var(--g400);margin-left:6px">Next 30 days</span></span></div>'+'<div class="card-body" style="padding:0 17px 4px" id="upcomingList"></div></div>'+'<div class="card"><div class="card-hdr"><span class="card-title"><i class="fas fa-history tgold"></i> Recent Activity</span>'+(isAdmin()?'<button class="btn btn-outline btn-sm" onclick="showPage(\'logs\')">Full Log</button>':'')+'</div>'
+'<div class="card-body" style="padding:0"><div class="tbl-wrap"><table><thead><tr><th>Time</th><th>User</th><th>Action</th><th>Entity</th></tr></thead><tbody>'+(logs||'<tr><td colspan="4" style="text-align:center;padding:22px;color:var(--g400)">No activity yet</td></tr>')+'</tbody></table></div></div></div>';
renderGantt(phases);
// Destroy any stale chart instance on this canvas
var schEl=g('sch'); if(!schEl){console.warn('canvas #sch not found');return;}
var existingChart=Chart.getChart(schEl);
if(existingChart){existingChart.destroy();}
if(sc){try{sc.destroy();}catch(e){}sc=null;}
var inp=Math.max(0,total-done-delayed);
var cv=g('chartCentreVal'); if(cv)cv.textContent=total;
var notStarted2=Math.max(0,total-done-inp-delayed);
var COLORS=['rgba(16,185,129,.92)','rgba(99,102,241,.92)','rgba(244,63,94,.92)','rgba(148,163,184,.85)'];
var COLORS_H=['#059669','#4f46e5','#e11d48','#64748b'];
var CLABELS=['Completed','In Progress','Delayed','Not Started'];
var CVALS=[done,inp,delayed,notStarted2];
sc=new Chart(schEl,{type:'doughnut',
data:{labels:CLABELS,datasets:[{
data:CVALS,
backgroundColor:COLORS,
hoverBackgroundColor:COLORS_H,
borderWidth:4,borderColor:'#fff',hoverBorderColor:'#fff',hoverBorderWidth:4,
hoverOffset:14,borderRadius:6,spacing:3
}]},
options:{responsive:true,maintainAspectRatio:false,
animation:{animateRotate:true,animateScale:true,duration:950,easing:'easeOutQuart'},
plugins:{
legend:{position:'bottom',labels:{font:{size:11,weight:'700'},padding:16,usePointStyle:true,pointStyleWidth:12,color:'#475569'}},
tooltip:{backgroundColor:'#0f172a',titleColor:'#fff',bodyColor:'rgba(255,255,255,.8)',padding:12,cornerRadius:10,boxWidth:10,boxHeight:10,
callbacks:{title:function(i){return i[0].label;},label:function(ctx){var v=ctx.raw||0,t=total||1;return ' '+v+' ('+Math.round(v/t*100)+'%)';}}}
},
cutout:'70%',
onHover:function(e,els){
var cv2=g('chartCentreVal'),cl2=g('chartCentreLbl');
if(!cv2||!cl2)return;
if(els.length){
var i=els[0].index;
cv2.textContent=CVALS[i]; cv2.style.color=COLORS_H[i];
cl2.textContent=CLABELS[i].toUpperCase();
}else{
cv2.textContent=total; cv2.style.color='#0f172a';
cl2.textContent='TOTAL';
}
}
}
});
renderUpcomingActivities();
renderActivityStatTiles(total,done,inp,delayed);
}
function renderActivityStatTiles(total,done,inp,delayed){
if(!g('actStatTiles'))return;
var box=g('actStatTiles'); if(!box)return;
var notStarted=Math.max(0,total-done-inp-delayed);
var pctDone=total?Math.round(done/total*100):0;
var pctInp=total?Math.round(inp/total*100):0;
var pctDel=total?Math.round(delayed/total*100):0;
var pctNs=total?Math.round(notStarted/total*100):0;
var tiles=[
{val:total, lbl:'Total', sub:'All in scope', pct:100, bg:'linear-gradient(135deg,#eef2ff,#e0e7ff)',border:'#c7d2fe',ic:'fas fa-layer-group', icBg:'linear-gradient(135deg,#6366f1,#4f46e5)',bar:'#6366f1'},
{val:done, lbl:'Completed', sub:pctDone+'% done', pct:pctDone, bg:'linear-gradient(135deg,#ecfdf5,#d1fae5)',border:'#a7f3d0',ic:'fas fa-circle-check', icBg:'linear-gradient(135deg,#10b981,#059669)',bar:'#10b981'},
{val:inp, lbl:'In Progress', sub:pctInp+'% active', pct:pctInp, bg:'linear-gradient(135deg,#eff6ff,#dbeafe)',border:'#bfdbfe',ic:'fas fa-bolt', icBg:'linear-gradient(135deg,#3b82f6,#2563eb)',bar:'#3b82f6'},
{val:delayed, lbl:'Overdue', sub:pctDel+'% late', pct:pctDel, bg:'linear-gradient(135deg,#fff1f2,#ffe4e6)',border:'#fecdd3',ic:'fas fa-circle-exclamation',icBg:'linear-gradient(135deg,#f43f5e,#e11d48)',bar:'#f43f5e'},
];
box.innerHTML=tiles.map(function(t){
return '<div class="stat-tile" style="background:'+t.bg+';border-color:'+t.border+'">'
+'<div class="stat-tile-icon" style="background:'+t.icBg+'"><i class="'+t.ic+'"></i></div>'
+'<div style="flex:1;min-width:0">'
+'<div class="stat-tile-val">'+t.val+'</div>'
+'<div class="stat-tile-lbl">'+t.lbl+'</div>'
+'<div style="height:3px;border-radius:2px;background:#e2e8f0;margin-top:6px">'
+'<div style="height:100%;border-radius:2px;background:'+t.bar+';width:'+Math.min(100,t.pct)+'%;transition:width .6s ease"></div>'
+'</div>'
+'<div class="stat-tile-pct" style="color:'+t.bar+'">'+t.sub+'</div>'
+'</div></div>';
}).join('');
}
function renderUpcomingActivities(){
var box=g('upcomingList'); if(!box)return;
var td=today();
var in30=addDays(td,30);
var statusColors={completed:'#15803d',delayed:'#b91c1c',in_progress:'#1d4ed8',not_started:'#6b7280'};
var upcoming=DB.activities.filter(function(a){
return a.endDate&&a.endDate>=td&&a.endDate<=in30&&a.status!=='completed';
}).sort(function(a,b){return a.endDate>b.endDate?1:-1;});
if(!upcoming.length){
box.innerHTML='<div class="empty" style="padding:18px 0"><i class="fas fa-calendar-check"></i><p style="margin:6px 0 0">No activities due in the next 30 days.</p></div>';
return;
}
var rows=upcoming.map(function(a){
var daysLeft=Math.round((new Date(a.endDate)-new Date(td))/(1000*60*60*24));
var mil=DB.milestones.find(function(m){return m.id===a.milestoneId;})||{};
var ph=DB.phases.find(function(p){return p.id===mil.phaseId;})||{};
var phIdx=DB.phases.indexOf(ph);
var phColor=PC[phIdx>=0?phIdx%PC.length:0];
var sc2=statusColors[a.status]||'#6b7280';
var urgency=daysLeft<=3?'color:#b91c1c;font-weight:800':daysLeft<=7?'color:#d97706;font-weight:700':'color:var(--g400)';
var dueLabel=daysLeft===0?'Due today':daysLeft===1?'Due tomorrow':'In '+daysLeft+' days';
return '<li class="upcoming-item">'
+'<div class="upcoming-dot" style="background:'+sc2+'"></div>'
+'<div style="flex:1;min-width:0">'
+'<div style="font-size:12.5px;font-weight:600;color:var(--navy);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">'+a.title+'</div>'
+'<div style="font-size:10.5px;color:var(--g400);margin-top:2px">'
+(ph.name?'<span style="color:'+phColor+';font-weight:600">Phase '+(ph.num||'')+'</span> · ':'')+(mil.title||'')+'</div>'
+'</div>'
+'<div style="text-align:right;flex-shrink:0">'
+'<div class="upcoming-days" style="'+urgency+'">'+dueLabel+'</div>'
+'<div style="font-size:10px;color:var(--g400);margin-top:1px">'+a.endDate+'</div>'
+'</div>'
+'</li>';
}).join('');
box.innerHTML='<ul class="upcoming-list">'+rows+'</ul>';
}
/* ?? PHASES ?? */
function renderPhases(){
var phases=DB.phases.slice().sort(function(a,b){return a.order-b.order;});
var html=phases.map(function(p,i){
var pp=phasePct(p.id);
var mils=DB.milestones.filter(function(m){return m.phaseId===p.id;});
var mDone=mils.filter(function(m){var a=DB.activities.filter(function(x){return x.milestoneId===m.id;});return a.length&&a.every(function(x){return x.status==='completed';});}).length;
var pills=mils.slice(0,6).map(function(m){
var c=m.status==='completed'?'var(--success)':m.status==='in_progress'?'var(--info)':m.status==='delayed'?'var(--danger)':'var(--g400)';
return '<span class="pill" onclick="showPage(\'milestoneDetail\','+m.id+')">'
+'<span style="width:7px;height:7px;border-radius:50%;flex-shrink:0;background:'+c+'"></span>'
+(m.title.length>35?m.title.slice(0,35)+'...':m.title)+'</span>';
}).join('');
return '<div class="card" style="border-left:4px solid '+PC[i%PC.length]+'">'
+'<div class="card-hdr"><div>'
+'<div style="font-size:10px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:'+PC[i%PC.length]+';margin-bottom:3px">Phase '+p.num+'</div>'
+'<span class="card-title">'+p.name+'</span>'+(p.desc?'<div class="muted fs12 mt-2">'+p.desc+'</div>':'')+'</div>'
+'<div class="flex ic gap-2"><span style="font-size:18px;font-weight:700;color:'+PC[i%PC.length]+'">'+pp+'%</span>'
+'<button class="btn btn-outline btn-sm" onclick="showPage(\'phaseDetail\','+p.id+')"><i class="fas fa-eye"></i> View</button>'
+(isEditor()?'<button class="btn btn-outline btn-sm" onclick="openPM('+p.id+')"><i class="fas fa-pencil"></i></button>':'')
+(isAdmin()?'<button class="btn btn-danger btn-sm" onclick="delPhase('+p.id+')"><i class="fas fa-trash"></i></button>':'')
+'</div></div>'
+'<div class="card-body"><div class="prog-wrap mb-3"><div class="prog-bar" style="width:'+pp+'%;background:'+PC[i%PC.length]+'"></div></div>'
+'<div class="flex gap-3 fs13 mb-3" style="color:var(--g600)"><span><i class="fas fa-flag tgold"></i> '+mils.length+' milestones</span><span><i class="fas fa-check" style="color:var(--success)"></i> '+mDone+' completed</span></div>'
+(mils.length?'<div style="display:flex;flex-wrap:wrap;gap:6px">'+pills+(mils.length>6?'<span class="pill tgold" onclick="showPage(\'phaseDetail\','+p.id+')">+' +(mils.length-6)+' more</span>':'')+'</div>':'')
+'</div></div>';
}).join('');
g('mainContent').innerHTML=
'<div class="flex jb ic mb-4"><p class="muted">All project phases and milestone progress</p>'+(isEditor()?'<button class="btn btn-gold" onclick="openPM()"><i class="fas fa-plus"></i> Add Phase</button>':'')+'</div>'
+html+'<div class="overlay" id="phMod"><div class="modal" style="max-width:480px">'
+'<div class="modal-hdr"><span class="modal-title" id="phMT">Add Phase</span><button class="modal-close" onclick="closeModal(\'phMod\')">&#x2715;</button></div>'
+'<div class="modal-body"><input type="hidden" id="phMId">'
+'<div class="fg-2"><div class="fg"><label>Phase Number</label><input type="number" id="phMN"></div><div class="fg"><label>Display Order</label><input type="number" id="phMO"></div></div>'
+'<div class="fg"><label>Phase Name *</label><input type="text" id="phMNm"></div>'
+'<div class="fg"><label>Description</label><textarea id="phMD"></textarea></div></div>'
+'<div class="modal-foot"><button class="btn btn-outline" onclick="closeModal(\'phMod\')">Cancel</button><button class="btn btn-primary" onclick="savePhase()"><i class="fas fa-save"></i> Save</button></div>'
+'</div></div>';
}
function openPM(id){
g('phMT').textContent=id?'Edit Phase':'Add Phase';g('phMId').value=id||'';
if(id){var p=DB.phases.find(function(x){return x.id===id;});g('phMN').value=p.num;g('phMO').value=p.order;g('phMNm').value=p.name;g('phMD').value=p.desc||'';}
else{['phMN','phMO','phMNm','phMD'].forEach(function(x){g(x).value='';});}
openModal('phMod');
}
function savePhase(){
var name=g('phMNm').value.trim();if(!name){alert('Name required.');return;}
var id=parseInt(g('phMId').value)||null;
if(id){var p=DB.phases.find(function(x){return x.id===id;});p.num=parseInt(g('phMN').value)||p.num;p.name=name;p.order=parseInt(g('phMO').value)||p.order;p.desc=g('phMD').value;addLog('EDIT_PHASE','phase',id,name);}
else{var np={id:nid('phase'),num:parseInt(g('phMN').value)||DB.phases.length+1,name:name,desc:g('phMD').value,order:parseInt(g('phMO').value)||DB.phases.length+1};DB.phases.push(np);addLog('ADD_PHASE','phase',np.id,name);}
saveDB(true,'Phase "'+(name)+'" was updated by '+CU.fullName);closeModal('phMod');renderPhases();flash('Phase saved.');
}
function delPhase(id){
if(!confirm('Delete this phase and ALL its data?'))return;
var mids=DB.milestones.filter(function(m){return m.phaseId===id;}).map(function(m){return m.id;});
var aids=DB.activities.filter(function(a){return mids.indexOf(a.milestoneId)>=0;}).map(function(a){return a.id;});
DB.deliverables=DB.deliverables.filter(function(d){return aids.indexOf(d.activityId)<0&&mids.indexOf(d.milestoneId)<0;});
DB.activities=DB.activities.filter(function(a){return mids.indexOf(a.milestoneId)<0;});
DB.milestones=DB.milestones.filter(function(m){return m.phaseId!==id;});
var p=DB.phases.find(function(x){return x.id===id;});addLog('DELETE_PHASE','phase',id,p?p.name:'');
DB.phases=DB.phases.filter(function(x){return x.id!==id;});saveDB();renderPhases();flash('Phase deleted.','danger');
}
/* ?? PHASE DETAIL ?? */
function renderPhaseDetail(pid){
var phase=DB.phases.find(function(p){return p.id===pid;});if(!phase){showPage('phases');return;}
g('pageTitle').textContent='Phase '+phase.num+': '+phase.name;
var milestones=DB.milestones.filter(function(m){return m.phaseId===pid;}).sort(function(a,b){return (a.serial||0)-(b.serial||0);});
var pp=phasePct(pid);
var mCards=milestones.map(function(m){
var mp=milestonePct(m.id);
var acts=DB.activities.filter(function(a){return a.milestoneId===m.id;});
var pills=acts.map(function(a){
var bc=a.status==='completed'?'var(--sbg)':a.status==='delayed'?'var(--dbg)':a.status==='in_progress'?'var(--ibg)':'var(--g100)';
var tc=a.status==='completed'?'var(--success)':a.status==='delayed'?'var(--danger)':a.status==='in_progress'?'var(--info)':'var(--g600)';
return '<span style="padding:2px 8px;border-radius:10px;font-size:11px;background:'+bc+';color:'+tc+'">'+(a.title.length>45?a.title.slice(0,45)+'...':a.title)+'</span>';
}).join('');
var bc=m.status==='completed'?'var(--success)':m.status==='delayed'?'var(--danger)':m.status==='in_progress'?'var(--info)':'var(--g300)';
return '<div class="card" style="border-left:3px solid '+bc+'">'
+'<div class="card-hdr"><div>'
+'<div class="flex ic gap-2 mb-1">'+(m.serial?'<span class="badge b-navy" style="font-size:10px">#'+m.serial+'</span>':'')+sBadge(m.status)+'</div>'
+'<span style="font-family:\'Playfair Display\',serif;font-size:14px;font-weight:600;color:var(--navy);cursor:pointer" onclick="showPage(\'milestoneDetail\','+m.id+')" onmouseover="this.style.color=\'var(--gold)\'" onmouseout="this.style.color=\'var(--navy)\'">'+m.title+'</span>'
+'<div class="approval-row">'
+(m.pmtStatus&&m.pmtStatus!=='not_reviewed'?'<span class="approval-badge ab-pmt-'+m.pmtStatus+'"><i class="fas fa-users-gear" style="font-size:8px"></i> PMT: '+(m.pmtStatus.replace(/_/g,' '))+'</span>':'')
+(m.pscStatus&&m.pscStatus!=='not_required'?'<span class="approval-badge ab-psc-'+m.pscStatus+'"><i class="fas fa-people-roof" style="font-size:8px"></i> PSC: '+(m.pscStatus.replace(/_/g,' '))+'</span>':'')
+'</div>'
+(m.owner?'<div class="muted fs12 mt-1"><i class="fas fa-user"></i> '+m.owner+'</div>':'')+'</div>'
+'<div class="flex ic gap-2">'+(m.tentDate?'<span class="muted fs12">'+m.tentDate+'</span>':'')+'<span style="font-weight:700;color:var(--navy)">'+mp+'%</span>'
+'<button class="btn btn-outline btn-xs" onclick="showPage(\'milestoneDetail\','+m.id+')"><i class="fas fa-eye"></i></button>'
+(isEditor()?'<button class="btn btn-outline btn-xs" onclick="openMM('+pid+','+m.id+')"><i class="fas fa-pencil"></i></button>'
+'<button class="btn btn-danger btn-xs" onclick="delMilestone('+m.id+','+pid+')"><i class="fas fa-trash"></i></button>':'')
+'</div></div>'
+'<div class="card-body" style="padding:10px 17px"><div class="prog-wrap" style="height:5px;margin-bottom:8px"><div class="prog-bar '+(mp>=100?'ps':'')+'" style="width:'+mp+'%"></div></div>'
+(acts.length?'<div style="display:flex;flex-wrap:wrap;gap:5px">'+pills+'</div>':'<span class="muted fs12">No activities yet.</span>')
+'</div></div>';
}).join('');
g('mainContent').innerHTML=
'<div class="bc"><span class="lnk" onclick="showPage(\'phases\')">Phases</span><i class="fas fa-chevron-right" style="font-size:9px"></i><span>Phase '+phase.num+'</span></div>'
+'<div class="flex jb ic mb-4"><p class="muted fs12">'+pp+'% complete &nbsp;&#183;&nbsp; '+milestones.length+' milestones</p>'
+'<div class="flex gap-2">'+(isEditor()?'<button class="btn btn-outline btn-sm" onclick="openPM('+pid+')"><i class="fas fa-pencil"></i> Edit</button><button class="btn btn-gold btn-sm" onclick="openMM('+pid+')"><i class="fas fa-plus"></i> Add Milestone</button>':'')+'</div></div>'
+'<div class="card"><div class="card-body" style="padding:13px 17px">'
+'<div class="flex jb ic mb-2"><span class="fw6 fs13">Phase Progress</span><span style="font-weight:700;color:var(--navy)">'+pp+'%</span></div>'
+'<div class="prog-wrap"><div class="prog-bar" style="width:'+pp+'%"></div></div>'
+'<div class="flex gap-4 mt-2 fs12 muted"><span><i class="fas fa-flag tgold"></i> '+milestones.length+' milestones</span>'
+'<span><i class="fas fa-check" style="color:var(--success)"></i> '+milestones.filter(function(m){return m.status==='completed';}).length+' completed</span>'
+'<span><i class="fas fa-exclamation" style="color:var(--danger)"></i> '+milestones.filter(function(m){return isDelayed(m.endDate,m.status);}).length+' delayed</span></div>'
+'</div></div>'
+mCards+'<div id="mmModAnchor"></div><div id="amModAnchor"></div>';
renderMilestoneModal(pid);renderActivityModal(0);
// Ensure phase edit modal is available on phase detail page
if(!g('phMod')){
var phOverlay=document.createElement('div');
phOverlay.className='overlay';phOverlay.id='phMod';
var phMod=document.createElement('div');phMod.className='modal';phMod.style.maxWidth='480px';
var phHdr=document.createElement('div');phHdr.className='modal-hdr';
phHdr.innerHTML='<span class="modal-title" id="phMT">Edit Phase</span>';
var phX=document.createElement('button');phX.className='modal-close';phX.innerHTML='&#x2715;';
phX.onclick=function(){closeModal('phMod');};phHdr.appendChild(phX);phMod.appendChild(phHdr);
var phBod=document.createElement('div');phBod.className='modal-body';
phBod.innerHTML='<input type="hidden" id="phMId">'
+'<div class="fg-2"><div class="fg"><label>Phase Number</label><input type="number" id="phMN"></div>'
+'<div class="fg"><label>Display Order</label><input type="number" id="phMO"></div></div>'
+'<div class="fg"><label>Phase Name *</label><input type="text" id="phMNm"></div>'
+'<div class="fg"><label>Description</label><textarea id="phMD"></textarea></div>';
phMod.appendChild(phBod);
var phFoot=document.createElement('div');phFoot.className='modal-foot';
var phCan=document.createElement('button');phCan.className='btn btn-outline';phCan.textContent='Cancel';
phCan.onclick=function(){closeModal('phMod');};phFoot.appendChild(phCan);
var phSav=document.createElement('button');phSav.className='btn btn-primary';
phSav.innerHTML='<i class="fas fa-save"></i> Save';
phSav.onclick=function(){savePhase();};phFoot.appendChild(phSav);
phMod.appendChild(phFoot);phOverlay.appendChild(phMod);document.body.appendChild(phOverlay);
}
}
/* ?? MILESTONE MODAL ?? */
function renderMilestoneModal(pid){
var phOpts=DB.phases.slice().sort(function(a,b){return a.order-b.order;}).map(function(p){return '<option value="'+p.id+'">Phase '+p.num+': '+p.name+'</option>';}).join('');
var el=document.createElement('div');
el.innerHTML='<div class="overlay" id="mmMod"><div class="modal">'
+'<div class="modal-hdr"><span class="modal-title" id="mmT">Add Milestone</span><button class="modal-close" onclick="closeModal(\'mmMod\')">&#x2715;</button></div>'
+'<div class="modal-body"><input type="hidden" id="mmId"><input type="hidden" id="mmPid" value="'+pid+'">'
+'<div class="fg-2"><div class="fg"><label>Phase *</label><select id="mmPh">'+phOpts+'</select></div><div class="fg"><label>Serial No.</label><input type="number" id="mmSer"></div></div>'
+'<div class="fg"><label>Title *</label><input type="text" id="mmTi"></div>'
+'<div class="fg"><label>Description</label><textarea id="mmDe"></textarea></div>'
+'<div class="fg-3"><div class="fg"><label>Status</label><select id="mmSt"><option value="not_started">Not Started</option><option value="in_progress">In Progress</option><option value="completed">Completed</option><option value="delayed">Delayed</option></select></div>'
+'<div class="fg"><label>Priority</label><select id="mmPr"><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option><option value="critical">Critical</option></select></div>'
+'<div class="fg"><label>Month</label><input type="text" id="mmMo" placeholder="e.g. 1-3"></div></div>'
+'<div class="fg-3"><div class="fg"><label>Tentative Date</label><input type="date" id="mmTD"></div><div class="fg"><label>Start Date</label><input type="date" id="mmSD"></div><div class="fg"><label>End Date</label><input type="date" id="mmED"></div></div>'
+'<div class="fg"><label>Owner</label><input type="text" id="mmOw"></div>'
+'<div class="fg"><label>Notes</label><textarea id="mmNo"></textarea></div></div>'
+'<div class="modal-foot"><button class="btn btn-outline" onclick="closeModal(\'mmMod\')">Cancel</button><button class="btn btn-primary" onclick="saveMilestone()"><i class="fas fa-save"></i> Save</button></div>'
+'</div></div>';
document.body.appendChild(el.firstElementChild);
}
function openMM(pid,mid){
if(!g('mmMod'))renderMilestoneModal(pid);
g('mmT').textContent=mid?'Edit Milestone':'Add Milestone';
g('mmId').value=mid||'';g('mmPid').value=pid;
if(mid){var m=DB.milestones.find(function(x){return x.id===mid;});
if(g('mmPh'))g('mmPh').value=m.phaseId;
g('mmSer').value=m.serial||'';g('mmTi').value=m.title;g('mmDe').value=m.desc||'';
g('mmSt').value=m.status;g('mmPr').value=m.priority;g('mmMo').value=m.month||'';
g('mmTD').value=m.tentDate||'';g('mmSD').value=m.startDate||'';g('mmED').value=m.endDate||'';
g('mmOw').value=m.owner||'';g('mmNo').value=m.notes||'';
} else {
['mmSer','mmTi','mmDe','mmMo','mmTD','mmSD','mmED','mmOw','mmNo'].forEach(function(x){if(g(x))g(x).value='';});
if(g('mmSt'))g('mmSt').value='not_started';if(g('mmPr'))g('mmPr').value='medium';
if(g('mmPh'))g('mmPh').value=pid;
}
openModal('mmMod');
}
function saveMilestone(){
var title=g('mmTi').value.trim();if(!title){alert('Title required.');return;}
var id=parseInt(g('mmId').value)||null;
var pid=parseInt(g('mmPh')?g('mmPh').value:g('mmPid').value);
var d={phaseId:pid,serial:parseInt(g('mmSer').value)||null,title:title,desc:g('mmDe').value,status:g('mmSt').value,priority:g('mmPr').value,month:g('mmMo').value,tentDate:g('mmTD').value||null,startDate:g('mmSD').value||null,endDate:g('mmED').value||null,owner:g('mmOw').value,notes:g('mmNo').value};
if(id){Object.assign(DB.milestones.find(function(x){return x.id===id;}),d);addLog('EDIT_MILESTONE','milestone',id,title);}
else{var nm=Object.assign({id:nid('milestone'),createdAt:new Date().toISOString()},d);DB.milestones.push(nm);addLog('ADD_MILESTONE','milestone',nm.id,title);}
saveDB();closeModal('mmMod');renderPhaseDetail(pid);flash('Milestone saved.');
}
function delMilestone(mid,pid){
if(!confirm('Delete milestone and all its activities?'))return;
var m=DB.milestones.find(function(x){return x.id===mid;});
var aids=DB.activities.filter(function(a){return a.milestoneId===mid;}).map(function(a){return a.id;});
DB.deliverables=DB.deliverables.filter(function(d){return aids.indexOf(d.activityId)<0&&d.milestoneId!==mid;});
DB.activities=DB.activities.filter(function(a){return a.milestoneId!==mid;});
DB.milestones=DB.milestones.filter(function(x){return x.id!==mid;});
addLog('DELETE_MILESTONE','milestone',mid,m?m.title:'');
saveDB();renderPhaseDetail(pid);flash('Milestone deleted.','danger');
}
/* ?? ACTIVITY MODAL ?? */
function renderActivityModal(mid){
if(g('amMod'))return;
var el=document.createElement('div');
el.innerHTML='<div class="overlay" id="amMod"><div class="modal">'
+'<div class="modal-hdr"><span class="modal-title" id="amT">Add Activity</span><button class="modal-close" onclick="closeModal(\'amMod\')">&#x2715;</button></div>'
+'<div class="modal-body"><input type="hidden" id="amId"><input type="hidden" id="amMid">'
+'<div class="fg"><label>Title *</label><input type="text" id="amTi"></div>'
+'<div class="fg"><label>Description</label><textarea id="amDe"></textarea></div>'
+'<div class="fg-3"><div class="fg"><label>Status</label><select id="amSt"><option value="not_started">Not Started</option><option value="in_progress">In Progress</option><option value="completed">Completed</option><option value="delayed">Delayed</option><option value="on_hold">On Hold</option></select></div>'
+'<div class="fg"><label>Progress % <span style=\"font-size:10px;color:var(--g400);text-transform:none;letter-spacing:0\">(auto or manual)</span></label><input type="number" id="amPr" min="0" max="100" value="0"><div style=\"font-size:11px;color:var(--g400);margin-top:3px\"><i class=\"fas fa-magic\"></i> Auto-calculated from dates when Start &amp; End are set</div></div>'
+'<div class="fg"><label>Month</label><input type="text" id="amMo"></div></div>'
+'<div class="fg-3"><div class="fg"><label>Tentative Date</label><input type="date" id="amTD"></div><div class="fg"><label>Start Date</label><input type="date" id="amSD"></div><div class="fg"><label>End Date</label><input type="date" id="amED"></div></div>'
+'<div class="fg"><label>Assigned To</label><input type="text" id="amAs"></div>'
+'<div class="fg"><label>Notes</label><textarea id="amNo"></textarea></div></div>'
+'<div class="modal-foot"><button class="btn btn-outline" onclick="closeModal(\'amMod\')">Cancel</button><button class="btn btn-primary" onclick="saveActivity()"><i class="fas fa-save"></i> Save</button></div>'
+'</div></div>';
document.body.appendChild(el.firstElementChild);
}
function openAM(mid,aid){
renderActivityModal(mid);
g('amT').textContent=aid?'Edit Activity':'Add Activity';
g('amId').value=aid||'';g('amMid').value=mid;
if(aid){var a=DB.activities.find(function(x){return x.id===aid;});
g('amTi').value=a.title;g('amDe').value=a.desc||'';g('amSt').value=a.status;g('amPr').value=a.progress||0;
g('amMo').value=a.month||'';g('amTD').value=a.tentDate||'';g('amSD').value=a.startDate||'';g('amED').value=a.endDate||'';
g('amAs').value=a.assignedTo||'';g('amNo').value=a.notes||'';
} else {
['amTi','amDe','amMo','amTD','amSD','amED','amAs','amNo'].forEach(function(x){g(x).value='';});
g('amSt').value='not_started';g('amPr').value=0;
}
openModal('amMod');
}
function saveActivity(){
var title=g('amTi').value.trim();if(!title){alert('Title required.');return;}
var id=parseInt(g('amId').value)||null;
var mid=parseInt(g('amMid').value);
var rawProg=parseInt(g('amPr').value)||0;
var autoP=calcProgress(g('amSD').value,g('amED').value,g('amSt').value);
var finalProg=(g('amSt').value==='completed')?100:(g('amSt').value==='not_started')?0:(autoP>0?autoP:rawProg);
var d={milestoneId:mid,title:title,desc:g('amDe').value,status:g('amSt').value,progress:finalProg,month:g('amMo').value,tentDate:g('amTD').value||null,startDate:g('amSD').value||null,endDate:g('amED').value||null,assignedTo:g('amAs').value,notes:g('amNo').value};
if(id){Object.assign(DB.activities.find(function(x){return x.id===id;}),d);addLog('EDIT_ACTIVITY','activity',id,title);}
else{var na=Object.assign({id:nid('activity'),createdAt:new Date().toISOString()},d);DB.activities.push(na);addLog('ADD_ACTIVITY','activity',na.id,title);}
saveDB();closeModal('amMod');renderMilestoneDetail(mid);flash('Activity saved.');
}
function delActivity(aid,mid){
if(!confirm('Delete activity?'))return;
var a=DB.activities.find(function(x){return x.id===aid;});
DB.deliverables=DB.deliverables.filter(function(d){return d.activityId!==aid;});
DB.activities=DB.activities.filter(function(x){return x.id!==aid;});
addLog('DELETE_ACTIVITY','activity',aid,a?a.title:'');
saveDB();renderMilestoneDetail(mid);flash('Deleted.','danger');
}
/* ?? FILE HELPERS ?? */
function attachRow(d,mid){
return '<div class="attach-row">'
+'<i class="fas '+fIcon(d.fileType||'')+'" style="color:var(--gold);font-size:14px;flex-shrink:0"></i>'
+'<div style="flex:1;min-width:0"><div class="fw6 fs13">'+d.title+'</div>'
+(d.fileName?'<div class="muted" style="font-size:11px"><i class="fas fa-paperclip" style="margin-right:3px"></i>'+d.fileName+(d.fileSize?' ('+Math.round(d.fileSize/1024)+'KB)':'')+'</div>':'<div class="muted" style="font-size:11px">No file attached</div>')
+(d.uploadedAt?'<div class="muted" style="font-size:11px">'+d.uploadedAt.slice(0,10)+'</div>':'')+'</div>'
+sBadge(d.approvalStatus||'pending')
+(d.fileData?'<button class="btn btn-outline btn-xs" onclick="dlFile('+d.id+')" title="Download"><i class="fas fa-download"></i></button>':'')
+(isAdmin()&&d.approvalStatus==='pending'?'<button class="btn btn-success btn-xs" onclick="approveD('+d.id+',\'approved\','+mid+')"><i class="fas fa-check"></i> Approve</button>':'')
+(isEditor()?'<button class="btn btn-danger btn-xs" onclick="delD('+d.id+','+mid+')"><i class="fas fa-trash"></i></button>':'')
+'</div>';
}
function toggleUp(id){var e=g(id);if(e)e.style.display=e.style.display==='none'?'block':'none';}
function dlFile(did){
var d=DB.deliverables.find(function(x){return x.id===did;});
if(!d||!d.fileData){alert('No file attached.');return;}
var a=document.createElement('a');a.href=d.fileData;a.download=d.fileName||'file';a.click();
addLog('DOWNLOAD_FILE','deliverable',did,d.fileName);saveDB();
}
function approveD(did,action,mid){
var d=DB.deliverables.find(function(x){return x.id===did;});if(!d)return;
d.approvalStatus=action;d.approvedBy=CU.id;d.approvedAt=new Date().toISOString();
addLog('APPROVE_FILE','deliverable',did,d.title);saveDB();renderMilestoneDetail(mid);flash('File '+action+'.');
}
function delD(did,mid){
if(!confirm('Delete this file/deliverable?'))return;
var d=DB.deliverables.find(function(x){return x.id===did;});
addLog('DELETE_FILE','deliverable',did,d?d.title:'');
DB.deliverables=DB.deliverables.filter(function(x){return x.id!==did;});
saveDB();renderMilestoneDetail(mid);flash('Deleted.','danger');
}
function doUpload(titleId,dateId,fileId,mid,aid){
var title=g(titleId).value.trim();if(!title){alert('Title required.');return;}
var fi=g(fileId);var file=fi?fi.files[0]:null;
var ok=['pdf','docx','xlsx','pptx','jpg','jpeg'];
var base={id:nid('deliverable'),milestoneId:mid,activityId:aid||null,title:title,dueDate:g(dateId)?g(dateId).value||null:null,approvalStatus:'pending',uploadedBy:CU.id,uploadedAt:new Date().toISOString()};
if(file){
var ext=file.name.split('.').pop().toLowerCase();
if(ok.indexOf(ext)<0){alert('Only pdf, docx, xlsx, pptx, jpg allowed.');return;}
if(file.size>10*1024*1024){alert('Max 10MB.');return;}
var r=new FileReader();
r.onload=function(e){
var d=Object.assign({},base,{fileName:file.name,fileType:ext,fileSize:file.size,fileData:e.target.result});
DB.deliverables.push(d);addLog('UPLOAD_FILE','deliverable',d.id,title);
DB.users.filter(function(u){return u.role==='admin'&&u.id!==CU.id;}).forEach(function(u){addNotif(u.id,'New file uploaded','"'+file.name+'" uploaded.','upload');});
saveDB();renderMilestoneDetail(mid);flash('File uploaded successfully.');
};r.readAsDataURL(file);
} else {
var d2=Object.assign({},base,{fileName:null,fileType:null,fileSize:null,fileData:null});
DB.deliverables.push(d2);addLog('ADD_DELIVERABLE','deliverable',d2.id,title);
saveDB();renderMilestoneDetail(mid);flash('Deliverable added.');
}
}
/* ?? MILESTONE DETAIL ?? */
function renderMilestoneDetail(mid){
var m=DB.milestones.find(function(x){return x.id===mid;});if(!m){showPage('phases');return;}
var phase=DB.phases.find(function(p){return p.id===m.phaseId;});
g('pageTitle').textContent='Milestone Detail';
var mp=milestonePct(mid);
var activities=DB.activities.filter(function(a){return a.milestoneId===mid;});
var comments=DB.comments.filter(function(c){return c.milestoneId===mid;}).sort(function(a,b){return a.createdAt.localeCompare(b.createdAt);});
var delayed=isDelayed(m.endDate,m.status);
var mFiles=DB.deliverables.filter(function(d){return d.milestoneId===mid&&!d.activityId;});
var mFilesHtml=mFiles.map(function(d){return attachRow(d,mid);}).join('');
var actHtml=activities.map(function(a){
var delivs=DB.deliverables.filter(function(d){return d.activityId===a.id;});
var dHtml=delivs.map(function(d){return attachRow(d,mid);}).join('');
var aBC=a.status==='completed'?'var(--success)':a.status==='delayed'?'var(--danger)':a.status==='in_progress'?'var(--info)':'var(--g300)';
return '<div class="card" style="border-left:3px solid '+aBC+'">'
+'<div class="card-hdr" style="padding:10px 15px">'
+'<div style="flex:1"><div class="flex ic gap-2">'+sBadge(a.status)+'<span class="fw6 fs13">'+a.title+'</span></div>'
+(a.assignedTo?'<div class="muted fs12 mt-1"><i class="fas fa-user"></i> '+a.assignedTo+'</div>':'')+'</div>'
+'<div class="flex ic gap-2">'+(a.endDate?'<span class="muted fs12"><i class="fas fa-calendar" style="margin-right:3px"></i>'+a.endDate+'</span>':'')
+(isEditor()?'<button class="btn btn-outline btn-xs" onclick="openAM('+mid+','+a.id+')"><i class="fas fa-pencil"></i></button>'
+'<button class="btn btn-danger btn-xs" onclick="delActivity('+a.id+','+mid+')"><i class="fas fa-trash"></i></button>':'')
+'</div></div>'
+'<div class="card-body" style="padding:10px 15px">'
+(a.desc?'<p class="muted fs12 mb-3">'+a.desc+'</p>':'')
+'<div class="flex jb ic mb-1"><span class="fs12 muted">Progress</span><span class="fs12 fw6">'+a.progress+'%</span></div>'
+'<div class="prog-wrap" style="height:5px;margin-bottom:12px"><div class="prog-bar '+(a.progress>=100?'ps':'')+'" style="width:'+a.progress+'%"></div></div>'
+'<div style="background:var(--g50);border:1px solid var(--g200);border-radius:8px;padding:12px">'
+'<div class="flex jb ic mb-2"><span style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--g600)"><i class="fas fa-paperclip tgold" style="margin-right:5px"></i>Files & Deliverables ('+delivs.length+')</span>'
+(isEditor()?'<button class="btn btn-gold btn-xs" onclick="toggleUp(\'aup-'+a.id+'\')"><i class="fas fa-upload"></i> Upload File</button>':'')+'</div>'
+(dHtml||'<p class="muted fs12">No files attached yet.</p>')
+(isEditor()?'<div id="aup-'+a.id+'" style="display:none;border-top:1px solid var(--g200);margin-top:10px;padding-top:10px">'
+'<div class="fg-2" style="margin-bottom:8px"><div class="fg"><label>Title *</label><input type="text" id="at-'+a.id+'" placeholder="e.g. Draft Report"></div><div class="fg"><label>Due Date</label><input type="date" id="ad-'+a.id+'"></div></div>'
+'<div class="fg" style="margin-bottom:8px"><label>Select File (pdf, docx, xlsx, pptx, jpg &mdash; max 10MB)</label>'
+'<input type="file" id="af-'+a.id+'" accept=".pdf,.docx,.xlsx,.pptx,.jpg,.jpeg" style="border:1px solid var(--g200);border-radius:6px;padding:5px 8px;font-size:12px"></div>'
+'<div class="flex gap-2"><button class="btn btn-primary btn-sm" onclick="doUpload(\'at-'+a.id+'\',\'ad-'+a.id+'\',\'af-'+a.id+'\','+mid+','+a.id+')"><i class="fas fa-upload"></i> Upload</button>'
+'<button class="btn btn-outline btn-sm" onclick="toggleUp(\'aup-'+a.id+'\')">Cancel</button></div></div>':'')
+'</div></div></div>';
}).join('');
var cHtml=comments.map(function(c){
var u=DB.users.find(function(x){return x.id===c.userId;});
var canEdit=isAdmin()||(CU&&CU.id===c.userId);
return '<div id="cmt-'+c.id+'" style="display:flex;gap:10px;margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid var(--g100)">'
+'<div style="width:28px;height:28px;border-radius:50%;background:var(--navy);display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px;font-weight:700;flex-shrink:0">'+(u?u.username:'?')[0].toUpperCase()+'</div>'
+'<div style="flex:1">'
+'<div class="flex jb ic"><div class="flex ic gap-2"><span class="fw6 fs13">'+(u?u.fullName||u.username:'?')+'</span><span class="muted fs12">'+c.createdAt.slice(0,16).replace('T',' ')+'</span>'+(c.edited?'<span class="muted fs12">(edited)</span>':'')+'</div>'
+(canEdit?'<div class="flex gap-1"><button class="btn btn-outline btn-xs" onclick="editComment('+c.id+','+mid+')"><i class="fas fa-pencil"></i></button><button class="btn btn-danger btn-xs" onclick="delComment('+c.id+','+mid+')"><i class="fas fa-trash"></i></button></div>':'')
+'</div>'
+'<div id="cmt-text-'+c.id+'" class="fs13 mt-2">'+c.content+'</div>'
+'<div id="cmt-edit-'+c.id+'" style="display:none;margin-top:8px">'
+'<textarea id="cmt-ta-'+c.id+'" rows="2" style="width:100%;padding:7px 10px;border:1px solid var(--g200);border-radius:6px;font-family:inherit;font-size:13px">'+c.content+'</textarea>'
+'<div class="flex gap-2 mt-2"><button class="btn btn-primary btn-xs" onclick="saveEditComment('+c.id+','+mid+')"><i class="fas fa-save"></i> Save</button><button class="btn btn-outline btn-xs" onclick="cancelEditComment('+c.id+')">Cancel</button></div>'
+'</div>'
+'</div></div>';
}).join('');
var bc=m.status==='completed'?'var(--success)':m.status==='delayed'?'var(--danger)':m.status==='in_progress'?'var(--info)':'var(--g300)';
g('mainContent').innerHTML=
'<div class="bc"><span class="lnk" onclick="showPage(\'phases\')">Phases</span><i class="fas fa-chevron-right" style="font-size:9px"></i>'
+(phase?'<span class="lnk" onclick="showPage(\'phaseDetail\','+m.phaseId+')">Phase '+phase.num+'</span>':'')
+'<i class="fas fa-chevron-right" style="font-size:9px"></i><span>'+(m.title.length>40?m.title.slice(0,40)+'...':m.title)+'</span></div>'
+'<div class="card" style="border-left:4px solid '+bc+'">'
+'<div class="card-hdr"><div>'
+'<div class="flex ic gap-2 mb-1">'+(m.serial?'<span class="badge b-navy">#'+m.serial+'</span>':'')+sBadge(m.status)+pBadge(m.priority)+(delayed?'<span class="badge b-danger"><i class="fas fa-exclamation-triangle"></i> Overdue</span>':'')+'</div>'
+'<h2 style="font-family:\'Playfair Display\',serif;font-size:18px;color:var(--navy)">'+m.title+'</h2>'
+(m.desc?'<p class="muted mt-2">'+m.desc+'</p>':'')+'</div>'
+(isEditor()?'<button class="btn btn-outline btn-sm" onclick="openMM('+m.phaseId+','+m.id+')"><i class="fas fa-pencil"></i> Edit</button>':'')+'</div>'
+'<div class="card-body">'
+'<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:13px;margin-bottom:13px">'
+(m.owner?'<div><div class="fs12 muted mb-1">Owner</div><div class="fw6 fs13">'+m.owner+'</div></div>':'')
+(m.month?'<div><div class="fs12 muted mb-1">Month</div><div class="fw6 fs13">Month '+m.month+'</div></div>':'')
+(m.tentDate?'<div><div class="fs12 muted mb-1">Tentative Date</div><div class="fw6 fs13">'+m.tentDate+'</div></div>':'')
+(m.endDate?'<div><div class="fs12 muted mb-1">End Date</div><div class="fw6 fs13 '+(delayed?'tgold':'')+'">'+m.endDate+'</div></div>':'')
+'</div>'
+'<div class="flex jb ic mb-1"><span class="fs12 muted">Completion</span><span class="fw6 fs13">'+mp+'%</span></div>'
+'<div class="prog-wrap"><div class="prog-bar '+(mp>=100?'ps':'')+'" style="width:'+mp+'%"></div></div>'
+'</div></div>'
+'<div class="card">'
+'<div class="card-hdr"><span class="card-title"><i class="fas fa-folder-open tgold"></i> Milestone Files & Deliverables <span class="badge b-navy" style="margin-left:6px;font-size:11px">'+mFiles.length+'</span></span>'
+(isEditor()?'<button class="btn btn-gold btn-sm" onclick="toggleUp(\'mup-'+mid+'\')"><i class="fas fa-upload"></i> Upload File</button>':'')+'</div>'
+'<div class="card-body">'
+(mFilesHtml||'<p class="muted fs12">No milestone-level files yet. Click Upload File to attach documents directly to this milestone.</p>')
+(isEditor()?'<div id="mup-'+mid+'" style="display:none;background:var(--g50);border:1px solid var(--g200);border-radius:8px;padding:14px;margin-top:12px">'
+'<div class="fw6 fs13 mb-3"><i class="fas fa-upload tgold"></i> Upload Milestone Document</div>'
+'<div class="fg-2" style="margin-bottom:8px"><div class="fg"><label>Document Title *</label><input type="text" id="mt-'+mid+'" placeholder="e.g. Approval Letter, Phase Report"></div><div class="fg"><label>Due Date</label><input type="date" id="md-'+mid+'"></div></div>'
+'<div class="fg" style="margin-bottom:10px"><label>Select File (pdf, docx, xlsx, pptx, jpg &mdash; max 10MB)</label>'
+'<input type="file" id="mf-'+mid+'" accept=".pdf,.docx,.xlsx,.pptx,.jpg,.jpeg" style="border:1px solid var(--g200);border-radius:6px;padding:5px 8px;font-size:12px"></div>'
+'<div class="flex gap-2"><button class="btn btn-primary" onclick="doUpload(\'mt-'+mid+'\',\'md-'+mid+'\',\'mf-'+mid+'\','+mid+',null)"><i class="fas fa-upload"></i> Upload to Milestone</button>'
+'<button class="btn btn-outline" onclick="toggleUp(\'mup-'+mid+'\')">Cancel</button></div></div>':'')
+'</div></div>'
+'<div class="flex jb ic mb-3"><h3 style="font-family:\'Playfair Display\',serif;font-size:15px;color:var(--navy)"><i class="fas fa-tasks tgold"></i> Activities</h3>'
+(isEditor()?'<button class="btn btn-gold btn-sm" onclick="openAM('+mid+')"><i class="fas fa-plus"></i> Add Activity</button>':'')+'</div>'
+(actHtml||'<div class="empty" style="padding:22px"><i class="fas fa-tasks"></i><p>No activities yet.</p></div>')
+'<div class="card mt-4"><div class="card-hdr"><span class="card-title"><i class="fas fa-comments tgold"></i> Comments &amp; Notes</span></div>'
+'<div class="card-body">'+cHtml
+(isEditor()?'<div class="fg mt-3"><label>Add Comment</label><textarea id="cTxt" rows="2" placeholder="Add a comment or note..."></textarea></div>'
+'<button class="btn btn-primary btn-sm" onclick="saveComment('+mid+')"><i class="fas fa-paper-plane"></i> Post</button>':'')
+'</div></div>';
renderMilestoneModal(m.phaseId);renderActivityModal(mid);
}
function saveComment(mid){
var txt=g('cTxt')?g('cTxt').value.trim():'';if(!txt){alert('Comment cannot be empty.');return;}
DB.comments.push({id:nid('comment'),milestoneId:mid,userId:CU.id,content:txt,createdAt:new Date().toISOString()});
addLog('ADD_COMMENT','milestone',mid);saveDB();renderMilestoneDetail(mid);flash('Comment added.');
}
/* ?? DECISIONS ?? */
function renderDecisions(){
var decs=DB.decisions.slice().sort(function(a,b){return b.createdAt.localeCompare(a.createdAt);});
var rows=decs.map(function(d){
var u=DB.users.find(function(x){return x.id===d.raisedBy;});
var p=DB.phases.find(function(x){return x.id===d.phaseId;});
return '<tr><td><div class="fw6 fs13">'+d.title+'</div>'+(d.description?'<div class="muted fs12">'+(d.description.length>65?d.description.slice(0,65)+'...':d.description)+'</div>':'')+'</td>'
+'<td>'+(d.type?'<span class="badge b-navy" style="font-size:10px">'+d.type.charAt(0).toUpperCase()+d.type.slice(1)+'</span>':'&#8212;')+'</td>'
+'<td>'+(p?'<span class="fs12">Ph.'+p.num+'</span>':'&#8212;')+'</td>'
+'<td>'+sBadge(d.status)+'</td><td>'+pBadge(d.priority)+'</td>'
+'<td class="fs12">'+(u?u.username:'&#8212;')+'</td>'
+'<td class="fs12">'+(d.dueDate||'&#8212;')+'</td>'
+'<td>'
+(d.fileData?'<button class="btn btn-outline btn-xs" onclick="dlDecFile(this.dataset.did)" data-did="'+d.id+'" title="Download document" style="margin-bottom:3px"><i class="fas fa-file-download"></i> Doc</button><br>':'')
+(isEditor()?'<div class="flex gap-1"><button class="btn btn-outline btn-xs" onclick="openDM('+d.id+')"><i class="fas fa-pencil"></i></button><button class="btn btn-danger btn-xs" onclick="delDec('+d.id+')"><i class="fas fa-trash"></i></button></div>':'')
+'</td></tr>';
}).join('');
var phOpts=DB.phases.slice().sort(function(a,b){return a.order-b.order;}).map(function(p){return '<option value="'+p.id+'">Phase '+p.num+': '+p.name+'</option>';}).join('');
g('mainContent').innerHTML=
'<div class="flex jb ic mb-4"><p class="muted">Track all project decisions and approvals</p>'+(isEditor()?'<button class="btn btn-gold" onclick="openDM()"><i class="fas fa-plus"></i> New Decision</button>':'')+'</div>'
+'<div class="stat-grid" style="grid-template-columns:repeat(4,1fr)">'
+'<div class="stat-card" style="--sc:#d97706;--scb:#fef3c7"><div class="stat-icon"><i class="fas fa-hourglass-half"></i></div><div><div class="stat-val">'+decs.filter(function(d){return d.status==='pending';}).length+'</div><div class="stat-lbl">Pending</div></div></div>'
+'<div class="stat-card" style="--sc:#15803d;--scb:#dcfce7"><div class="stat-icon"><i class="fas fa-check-circle"></i></div><div><div class="stat-val">'+decs.filter(function(d){return d.status==='approved';}).length+'</div><div class="stat-lbl">Approved</div></div></div>'
+'<div class="stat-card" style="--sc:#1d4ed8;--scb:#dbeafe"><div class="stat-icon"><i class="fas fa-clock-rotate-left"></i></div><div><div class="stat-val">'+decs.filter(function(d){return d.status==='deferred';}).length+'</div><div class="stat-lbl">Deferred</div></div></div>'
+'<div class="stat-card" style="--sc:#b91c1c;--scb:#fee2e2"><div class="stat-icon"><i class="fas fa-ban"></i></div><div><div class="stat-val">'+decs.filter(function(d){return d.status==='rejected';}).length+'</div><div class="stat-lbl">Rejected</div></div></div>'
+'</div>'
+'<div class="card"><div class="card-body" style="padding:0"><div class="tbl-wrap"><table>'
+'<thead><tr><th>Decision</th><th>Type</th><th>Phase</th><th>Status</th><th>Priority</th><th>Raised By</th><th>Due Date</th><th>Actions</th></tr></thead>'
+'<tbody>'+(rows||'<tr><td colspan="8" style="text-align:center;padding:26px;color:var(--g400)">No decisions recorded.</td></tr>')+'</tbody>'
+'</table></div></div></div>'
+'<div class="overlay" id="dMod"><div class="modal"><div class="modal-hdr"><span class="modal-title" id="dMT">New Decision</span><button class="modal-close" onclick="closeModal(\'dMod\')">&#x2715;</button></div>'
+'<div class="modal-body"><input type="hidden" id="dMId">'
+'<div class="fg"><label>Title *</label><input type="text" id="dMTi"></div>'
+'<div class="fg"><label>Description</label><textarea id="dMDe"></textarea></div>'
+'<div class="fg-2"><div class="fg"><label>Type</label><select id="dMTy"><option value="">&#8212; Select &#8212;</option><option value="strategic">Strategic</option><option value="operational">Operational</option><option value="financial">Financial</option><option value="risk">Risk</option><option value="technical">Technical</option><option value="resource">Resource</option></select></div>'
+'<div class="fg"><label>Priority</label><select id="dMPr"><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option><option value="critical">Critical</option></select></div></div>'
+'<div class="fg-2"><div class="fg"><label>Status</label><select id="dMSt"><option value="pending">Pending</option><option value="in_review">In Review</option><option value="approved">Approved</option><option value="rejected">Rejected</option><option value="deferred">Deferred</option></select></div>'
+'<div class="fg"><label>Due Date</label><input type="date" id="dMDu"></div></div>'
+'<div class="fg"><label>Related Phase</label><select id="dMPh"><option value="">&#8212; None &#8212;</option>'+phOpts+'</select></div>'
+'<div class="fg"><label>Notes / Rationale</label><textarea id="dMNo"></textarea></div></div>'
+'<div class="modal-foot"><button class="btn btn-outline" onclick="closeModal(\'dMod\')">Cancel</button><button class="btn btn-primary" onclick="saveDec()"><i class="fas fa-save"></i> Save</button></div>'
+'</div></div>';
}
function openDM(id){
g('dMT').textContent=id?'Edit Decision':'New Decision';g('dMId').value=id||'';
if(g('dMFi'))g('dMFi').value='';
if(id){
var d=DB.decisions.find(function(x){return x.id===id;});
g('dMTi').value=d.title;g('dMDe').value=d.description||'';g('dMTy').value=d.type||'';
g('dMPr').value=d.priority;g('dMSt').value=d.status;g('dMDu').value=d.dueDate||'';
g('dMPh').value=d.phaseId||'';g('dMNo').value=d.notes||'';
// Show existing document if any
if(d.fileName&&g('dMExisting')){
g('dMExisting').style.display='block';
var _er=g('dMExistingRow');_er.innerHTML='';
var _ar=document.createElement('div');_ar.className='attach-row';
_ar.innerHTML='<i class="fas '+fIcon(d.fileType||'')+'" style="color:var(--gold);font-size:14px;flex-shrink:0"></i>'
+'<div style="flex:1"><div class="fw6 fs12">'+(d.docTitle||d.fileName||'Document')+'</div>'
+'<div class="muted" style="font-size:11px">'+(d.fileName||'')+'</div></div>';
var _dlb=document.createElement('button');_dlb.className='btn btn-outline btn-xs';
_dlb.innerHTML='<i class="fas fa-download"></i> Download';
_dlb.onclick=(function(did){return function(){dlDecFile(did);};})(id);_ar.appendChild(_dlb);
var _rmb=document.createElement('button');_rmb.className='btn btn-danger btn-xs';
_rmb.innerHTML='<i class="fas fa-trash"></i> Remove';
_rmb.onclick=(function(did){return function(){removeDecFile(did);};})(id);_ar.appendChild(_rmb);
_er.appendChild(_ar);
} else if(g('dMExisting')){g('dMExisting').style.display='none';}
} else {
['dMTi','dMDe','dMDu','dMNo'].forEach(function(x){g(x).value='';});
g('dMTy').value='';g('dMPr').value='medium';g('dMSt').value='pending';g('dMPh').value='';
if(g('dMExisting'))g('dMExisting').style.display='none';
}
openModal('dMod');
}
function dlDecFile(id){
var d=DB.decisions.find(function(x){return x.id===parseInt(id);});
if(!d||!d.fileData){alert('No file attached.');return;}
var a=document.createElement('a');a.href=d.fileData;a.download=d.fileName||'document';a.click();
addLog('DOWNLOAD_DEC_FILE','decision',d.id,d.fileName);saveDB();
}
function removeDecFile(id){
if(!confirm('Remove the attached document?'))return;
var d=DB.decisions.find(function(x){return x.id===parseInt(id);});
if(!d)return;
d.fileName=null;d.fileData=null;d.fileType=null;d.fileSize=null;d.docTitle=null;
saveDB();openDM(parseInt(id));flash('Document removed.');
}
function saveDec(){
var title=g('dMTi').value.trim();if(!title){alert('Title required.');return;}
var id=parseInt(g('dMId').value)||null;
var d={title:title,description:g('dMDe').value,type:g('dMTy').value,priority:g('dMPr').value,status:g('dMSt').value,dueDate:g('dMDu').value||null,phaseId:parseInt(g('dMPh').value)||null,notes:g('dMNo').value};
var fi=g('dMFi');var file=fi?fi.files[0]:null;
var ok=['pdf','docx','xlsx','pptx','jpg','jpeg'];
function doSave(fileFields){
var finalD=fileFields?Object.assign(d,fileFields):d;
if(id){Object.assign(DB.decisions.find(function(x){return x.id===id;}),finalD);addLog('EDIT_DECISION','decision',id,title);}
else{var nd=Object.assign({id:nid('decision'),raisedBy:CU.id,createdAt:new Date().toISOString()},finalD);DB.decisions.push(nd);addLog('ADD_DECISION','decision',nd.id,title);DB.users.filter(function(u){return u.role==='admin';}).forEach(function(u){addNotif(u.id,'New decision pending','"'+title+'" needs attention.','approval');});}
saveDB();closeModal('dMod');renderDecisions();flash('Decision saved.');
}
if(file){
var ext=file.name.split('.').pop().toLowerCase();
if(ok.indexOf(ext)<0){alert('Only pdf, docx, xlsx, pptx, jpg allowed.');return;}
if(file.size>10*1024*1024){alert('Max 10MB.');return;}
var r=new FileReader();
r.onload=function(e){doSave({fileName:file.name,fileType:ext,fileSize:file.size,fileData:e.target.result,docTitle:title+' - Document'});};
r.readAsDataURL(file);
} else {doSave(null);}
}
function delDec(id){
if(!confirm('Delete decision?'))return;
var d=DB.decisions.find(function(x){return x.id===id;});addLog('DELETE_DECISION','decision',id,d?d.title:'');
DB.decisions=DB.decisions.filter(function(x){return x.id!==id;});saveDB();renderDecisions();flash('Deleted.','danger');
}
/* ?? RISKS ?? */
function renderRisks(){
var risks=DB.risks.slice().sort(function(a,b){return b.createdAt.localeCompare(a.createdAt);});
var pb={low:'b-gray',medium:'b-warn',high:'b-danger'};
var rows=risks.map(function(r){
var p=DB.phases.find(function(x){return x.id===r.phaseId;});
return '<tr><td><div class="fw6 fs13">'+r.title+'</div>'+(r.description?'<div class="muted fs12">'+(r.description.length>65?r.description.slice(0,65)+'...':r.description)+'</div>':'')+(r.mitigation?'<div class="fs12 mt-1" style="color:var(--success)"><i class="fas fa-shield-halved"></i> '+(r.mitigation.length>55?r.mitigation.slice(0,55)+'...':r.mitigation)+'</div>':'')+'</td>'
+'<td>'+(r.category?'<span class="badge b-navy" style="font-size:10px">'+r.category.charAt(0).toUpperCase()+r.category.slice(1)+'</span>':'&#8212;')+'</td>'
+'<td><span class="badge '+(pb[r.probability]||'b-gray')+'">'+(r.probability||'&#8212;').charAt(0).toUpperCase()+(r.probability||'').slice(1)+'</span></td>'
+'<td><span class="badge '+(pb[r.impact]||'b-gray')+'">'+(r.impact||'&#8212;').charAt(0).toUpperCase()+(r.impact||'').slice(1)+'</span></td>'
+'<td>'+sBadge(r.status)+'</td>'
+'<td class="fs12">'+(r.owner||'&#8212;')+'</td>'
+'<td>'+(p?'<span class="fs12">Ph.'+p.num+'</span>':'&#8212;')+'</td>'
+'<td>'+(isEditor()?'<div class="flex gap-1"><button class="btn btn-outline btn-xs" onclick="openRM('+r.id+')"><i class="fas fa-pencil"></i></button><button class="btn btn-danger btn-xs" onclick="delRisk('+r.id+')"><i class="fas fa-trash"></i></button></div>':'')+'</td></tr>';
}).join('');
var phOpts=DB.phases.slice().sort(function(a,b){return a.order-b.order;}).map(function(p){return '<option value="'+p.id+'">Phase '+p.num+': '+p.name+'</option>';}).join('');
g('mainContent').innerHTML=
'<div class="flex jb ic mb-4"><p class="muted">Monitor and manage project risks</p>'+(isEditor()?'<button class="btn btn-gold" onclick="openRM()"><i class="fas fa-plus"></i> Log Risk</button>':'')+'</div>'
+'<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:12px;margin-bottom:18px">'
+'<div style="text-align:center;padding:11px;background:var(--dbg);border-radius:8px"><div style="font-size:22px;font-weight:700;color:var(--danger)">'+risks.filter(function(r){return r.status==='open';}).length+'</div><div style="font-size:11px;color:var(--danger)">Open</div></div>'
+'<div style="text-align:center;padding:11px;background:var(--wbg);border-radius:8px"><div style="font-size:22px;font-weight:700;color:var(--warn)">'+risks.filter(function(r){return r.impact==='high';}).length+'</div><div style="font-size:11px;color:var(--warn)">High Impact</div></div>'
+'<div style="text-align:center;padding:11px;background:var(--ibg);border-radius:8px"><div style="font-size:22px;font-weight:700;color:var(--info)">'+risks.filter(function(r){return r.status==='mitigated';}).length+'</div><div style="font-size:11px;color:var(--info)">Mitigated</div></div>'
+'<div style="text-align:center;padding:11px;background:var(--sbg);border-radius:8px"><div style="font-size:22px;font-weight:700;color:var(--success)">'+risks.filter(function(r){return r.status==='closed';}).length+'</div><div style="font-size:11px;color:var(--success)">Closed</div></div>'
+'</div>'
+'<div class="card"><div class="card-body" style="padding:0"><div class="tbl-wrap"><table>'
+'<thead><tr><th>Risk</th><th>Category</th><th>Probability</th><th>Impact</th><th>Status</th><th>Owner</th><th>Phase</th><th>Actions</th></tr></thead>'
+'<tbody>'+(rows||'<tr><td colspan="8" style="text-align:center;padding:26px;color:var(--g400)">No risks logged.</td></tr>')+'</tbody>'
+'</table></div></div></div>'
+'<div class="overlay" id="rMod"><div class="modal"><div class="modal-hdr"><span class="modal-title" id="rMT">Log Risk</span><button class="modal-close" onclick="closeModal(\'rMod\')">&#x2715;</button></div>'
+'<div class="modal-body"><input type="hidden" id="rMId">'
+'<div class="fg"><label>Title *</label><input type="text" id="rMTi"></div>'
+'<div class="fg"><label>Description</label><textarea id="rMDe"></textarea></div>'
+'<div class="fg-3"><div class="fg"><label>Category</label><select id="rMCa"><option value="">&#8212; Select &#8212;</option><option value="financial">Financial</option><option value="operational">Operational</option><option value="technical">Technical</option><option value="regulatory">Regulatory</option><option value="reputational">Reputational</option><option value="resource">Resource</option><option value="timeline">Timeline</option></select></div>'
+'<div class="fg"><label>Probability</label><select id="rMPb"><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div>'
+'<div class="fg"><label>Impact</label><select id="rMIm"><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div></div>'
+'<div class="fg-2"><div class="fg"><label>Status</label><select id="rMSt"><option value="open">Open</option><option value="in_review">In Review</option><option value="mitigated">Mitigated</option><option value="accepted">Accepted</option><option value="closed">Closed</option></select></div>'
+'<div class="fg"><label>Owner</label><input type="text" id="rMOw"></div></div>'
+'<div class="fg"><label>Related Phase</label><select id="rMPh"><option value="">&#8212; None &#8212;</option>'+phOpts+'</select></div>'
+'<div class="fg"><label>Mitigation Plan</label><textarea id="rMMi"></textarea></div></div>'
+'<div class="modal-foot"><button class="btn btn-outline" onclick="closeModal(\'rMod\')">Cancel</button><button class="btn btn-primary" onclick="saveRisk()"><i class="fas fa-save"></i> Save</button></div>'
+'</div></div>';
}
function openRM(id){
g('rMT').textContent=id?'Edit Risk':'Log Risk';g('rMId').value=id||'';
if(id){var r=DB.risks.find(function(x){return x.id===id;});g('rMTi').value=r.title;g('rMDe').value=r.description||'';g('rMCa').value=r.category||'';g('rMPb').value=r.probability||'medium';g('rMIm').value=r.impact||'medium';g('rMSt').value=r.status;g('rMOw').value=r.owner||'';g('rMPh').value=r.phaseId||'';g('rMMi').value=r.mitigation||'';}
else{['rMTi','rMDe','rMOw','rMMi'].forEach(function(x){g(x).value='';});g('rMCa').value='';g('rMPb').value='medium';g('rMIm').value='medium';g('rMSt').value='open';g('rMPh').value='';}
openModal('rMod');
}
function saveRisk(){
var title=g('rMTi').value.trim();if(!title){alert('Title required.');return;}
var id=parseInt(g('rMId').value)||null;
var d={title:title,description:g('rMDe').value,category:g('rMCa').value,probability:g('rMPb').value,impact:g('rMIm').value,status:g('rMSt').value,owner:g('rMOw').value,phaseId:parseInt(g('rMPh').value)||null,mitigation:g('rMMi').value};
if(id){Object.assign(DB.risks.find(function(x){return x.id===id;}),d);addLog('EDIT_RISK','risk',id,title);}
else{var nr=Object.assign({id:nid('risk'),createdBy:CU.id,createdAt:new Date().toISOString()},d);DB.risks.push(nr);addLog('ADD_RISK','risk',nr.id,title);}
saveDB(true,'Risk "'+title+'" was '+(id?'updated':'added')+' by '+CU.fullName);closeModal('rMod');renderRisks();flash('Risk saved.');
}
function delRisk(id){
if(!confirm('Delete risk?'))return;
var r=DB.risks.find(function(x){return x.id===id;});addLog('DELETE_RISK','risk',id,r?r.title:'');
DB.risks=DB.risks.filter(function(x){return x.id!==id;});saveDB();renderRisks();flash('Deleted.','danger');
}
/* ?? USERS ?? */
function renderUsers(){
if(!isAdmin()){g('mainContent').innerHTML='<div class="empty"><i class="fas fa-lock"></i><p>Admin access required.</p></div>';return;}
var rows=DB.users.map(function(u){
return '<tr><td><div class="flex ic gap-2">'
+'<div style="width:27px;height:27px;border-radius:50%;background:var(--navy);display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px;font-weight:700">'+u.username[0].toUpperCase()+'</div>'
+'<div><div class="fw6 fs13">'+(u.fullName||u.username)+'</div><div class="muted fs12">@'+u.username+'</div></div></div></td>'
+'<td class="fs12">'+u.email+'</td><td class="fs12">'+(u.dept||'&#8212;')+'</td>'
+'<td><span class="badge '+(u.role==='admin'?'b-navy':u.role==='editor'?'b-info':'b-gray')+'">'+u.role.charAt(0).toUpperCase()+u.role.slice(1)+'</span></td>'
+'<td><span class="badge '+(u.active?'b-success':'b-danger')+'">'+(u.active?'Active':'Inactive')+'</span></td>'
+'<td class="muted fs12">'+(u.lastLogin?u.lastLogin.slice(0,16).replace('T',' '):'Never')+'</td>'
+'<td><div class="flex gap-1"><button class="btn btn-outline btn-xs" onclick="openUM('+u.id+')"><i class="fas fa-pencil"></i></button>'
+(u.id!==CU.id?'<button class="btn btn-danger btn-xs" onclick="delUser('+u.id+')"><i class="fas fa-trash"></i></button>':'')
+'</div></td></tr>';
}).join('');
g('mainContent').innerHTML=
'<div class="flex jb ic mb-4"><p class="muted">Manage users, roles and portal access</p><button class="btn btn-gold" onclick="openUM()"><i class="fas fa-user-plus"></i> Add User</button></div>'
+'<div class="stat-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:18px">'
+'<div class="stat-card" style="--sc:#c9a227;--scb:#f5e9c0"><div class="stat-icon"><i class="fas fa-users"></i></div><div><div class="stat-val">'+DB.users.length+'</div><div class="stat-lbl">Total Users</div></div></div>'
+'<div class="stat-card" style="--sc:#1a3a5c;--scb:#e0e7f0"><div class="stat-icon"><i class="fas fa-user-shield"></i></div><div><div class="stat-val">'+DB.users.filter(function(u){return u.role==='admin';}).length+'</div><div class="stat-lbl">Admins</div></div></div>'
+'<div class="stat-card" style="--sc:#1d4ed8;--scb:#dbeafe"><div class="stat-icon"><i class="fas fa-user-pen"></i></div><div><div class="stat-val">'+DB.users.filter(function(u){return u.role==='editor';}).length+'</div><div class="stat-lbl">Editors</div></div></div>'
+'<div class="stat-card" style="--sc:#15803d;--scb:#dcfce7"><div class="stat-icon"><i class="fas fa-eye"></i></div><div><div class="stat-val">'+DB.users.filter(function(u){return u.role==='viewer';}).length+'</div><div class="stat-lbl">Viewers</div></div></div>'
+'</div>'
+'<div class="card mb-4"><div class="card-hdr"><span class="card-title"><i class="fas fa-users tgold"></i> All Users</span></div>'
+'<div class="card-body" style="padding:0"><div class="tbl-wrap"><table>'
+'<thead><tr><th>User</th><th>Email</th><th>Department</th><th>Role</th><th>Status</th><th>Last Login</th><th>Actions</th></tr></thead>'
+'<tbody>'+rows+'</tbody></table></div></div></div>'
+'<div class="card"><div class="card-hdr"><span class="card-title"><i class="fas fa-bell tgold"></i> Send Notification</span></div><div class="card-body">'
+'<div class="fg"><label>Recipients</label><div style="display:flex;flex-wrap:wrap;gap:7px">'
+DB.users.map(function(u){return '<label style="display:flex;align-items:center;gap:6px;font-size:13px;text-transform:none;letter-spacing:0;cursor:pointer;padding:4px 9px;background:var(--g50);border:1px solid var(--g200);border-radius:6px"><input type="checkbox" name="nuid" value="'+u.id+'" style="width:auto;margin:0"> '+(u.fullName||u.username)+'</label>';}).join('')
+'</div></div>'
+'<div class="fg-2"><div class="fg"><label>Title *</label><input type="text" id="nTi" placeholder="Title"></div><div class="fg"><label>Message</label><input type="text" id="nMs" placeholder="Optional message"></div></div>'
+'<button class="btn btn-primary" onclick="sendNotif()"><i class="fas fa-paper-plane"></i> Send</button></div></div>'
+'<div class="overlay" id="uMod"><div class="modal"><div class="modal-hdr"><span class="modal-title" id="uMT">Add User</span><button class="modal-close" onclick="closeModal(\'uMod\')">&#x2715;</button></div>'
+'<div class="modal-body"><input type="hidden" id="uMId">'
+'<div class="fg-2"><div class="fg"><label>Username *</label><input type="text" id="uMUn"></div><div class="fg"><label>Full Name</label><input type="text" id="uMFn"></div></div>'
+'<div class="fg-2"><div class="fg"><label>Email *</label><input type="email" id="uMEm"></div><div class="fg"><label>Department</label><input type="text" id="uMDe"></div></div>'
+'<div class="fg-2"><div class="fg"><label>Role *</label><select id="uMRo"><option value="viewer">Viewer</option><option value="editor">Editor</option><option value="admin">Admin</option></select>'
+'<div class="muted fs12 mt-2">Admin: full &nbsp;&#183;&nbsp; Editor: modify &nbsp;&#183;&nbsp; Viewer: read-only</div></div>'
+'<div class="fg"><label>Active</label><div style="display:flex;align-items:center;gap:8px;margin-top:10px"><input type="checkbox" id="uMAc" checked style="width:auto"><label for="uMAc" style="font-size:13px;text-transform:none;letter-spacing:0;cursor:pointer;margin:0">Active account</label></div></div></div>'
+'<div class="divider"></div>'
+'<div class="fg-2"><div class="fg"><label id="uMPL">Password *</label><input type="password" id="uMPw"></div><div class="fg"><label>Confirm</label><input type="password" id="uMPw2"></div></div></div>'
+'<div class="modal-foot"><button class="btn btn-outline" onclick="closeModal(\'uMod\')">Cancel</button><button class="btn btn-primary" onclick="saveUser()"><i class="fas fa-save"></i> Save</button></div>'
+'</div></div>';
}
function openUM(id){
g('uMT').textContent=id?'Edit User':'Add User';g('uMId').value=id||'';
g('uMPL').textContent=id?'New Password (leave blank to keep)':'Password *';
if(id){var u=DB.users.find(function(x){return x.id===id;});g('uMUn').value=u.username;g('uMFn').value=u.fullName||'';g('uMEm').value=u.email;g('uMDe').value=u.dept||'';g('uMRo').value=u.role;g('uMAc').checked=u.active;g('uMPw').value='';g('uMPw2').value='';}
else{['uMUn','uMFn','uMEm','uMDe','uMPw','uMPw2'].forEach(function(x){g(x).value='';});g('uMRo').value='viewer';g('uMAc').checked=true;}
openModal('uMod');
}
function saveUser(){
var uname=g('uMUn').value.trim(),email=g('uMEm').value.trim();
var pw=g('uMPw').value,pw2=g('uMPw2').value;
var id=parseInt(g('uMId').value)||null;
if(!uname||!email){alert('Username and email required.');return;}
if(pw&&pw!==pw2){alert('Passwords do not match.');return;}
if(!id&&!pw){alert('Password required for new user.');return;}
if(!id&&DB.users.find(function(u){return u.username===uname;})){alert('Username already exists.');return;}
var oldRole=id?DB.users.find(function(u){return u.id===id;}).role:null;
var d={username:uname,email:email,fullName:g('uMFn').value,dept:g('uMDe').value,role:g('uMRo').value,active:g('uMAc').checked};
if(pw)d.password=pw;
if(id){Object.assign(DB.users.find(function(u){return u.id===id;}),d);addLog('EDIT_USER','user',id,uname,'Role:'+d.role);if(oldRole!==d.role)addNotif(id,'Permission Updated','Your role changed from '+oldRole+' to '+d.role+'.','permission');}
else{var nu=Object.assign({id:nid('user'),lastLogin:null},d);DB.users.push(nu);addLog('CREATE_USER','user',nu.id,uname);addNotif(nu.id,'Welcome to Pinnacle','Your account was created with role: '+d.role+'.','permission');}
saveDB(true,'User "'+uname+'" was '+(id?'updated':'added')+' by '+CU.fullName);closeModal('uMod');renderUsers();flash('User saved.');
}
function delUser(id){
if(id===CU.id){alert("Cannot delete your own account.");return;}
if(!confirm('Delete user?'))return;
var u=DB.users.find(function(x){return x.id===id;});addLog('DELETE_USER','user',id,u?u.username:'');
DB.users=DB.users.filter(function(x){return x.id!==id;});saveDB();renderUsers();flash('User deleted.','danger');
}
function sendNotif(){
var title=g('nTi').value.trim();if(!title){alert('Title required.');return;}
var msg=g('nMs').value;
var boxes=[].slice.call(document.querySelectorAll('input[name="nuid"]:checked'));
if(!boxes.length){alert('Select at least one recipient.');return;}
boxes.forEach(function(b){addNotif(parseInt(b.value),title,msg,'admin');});
saveDB();flash('Notification sent to '+boxes.length+' user(s).');
g('nTi').value='';g('nMs').value='';
document.querySelectorAll('input[name="nuid"]').forEach(function(b){b.checked=false;});
}
/* ?? LOGS ?? */
function renderLogs(){
if(!isAdmin()){g('mainContent').innerHTML='<div class="empty"><i class="fas fa-lock"></i><p>Admin access required.</p></div>';return;}
var logs=[].concat(DB.logs).reverse();
var rows=logs.map(function(l){
var u=DB.users.find(function(x){return x.id===l.userId;});
return '<tr><td class="muted fs12" style="white-space:nowrap">'+l.timestamp.slice(0,19).replace('T',' ')+'</td>'
+'<td><span class="badge b-navy" style="font-size:10px">'+(u?u.username:'System')+'</span></td>'
+'<td><span class="badge b-gold" style="font-size:10px">'+l.action+'</span></td>'
+'<td class="muted fs12">'+(l.entityType||'&#8212;')+'</td>'
+'<td class="fs12">'+((l.entityName||'').slice(0,40)||'&#8212;')+'</td>'
+'<td class="muted fs12">'+((l.details||'').slice(0,50)||'&#8212;')+'</td></tr>';
}).join('');
g('mainContent').innerHTML=
'<div class="flex jb ic mb-4"><p class="muted">Full system audit trail &mdash; '+logs.length+' entries</p>'
+'<button class="btn btn-outline btn-sm" onclick="exportLogs()"><i class="fas fa-file-csv"></i> Export CSV</button></div>'
+'<div class="card"><div class="card-body" style="padding:0"><div class="tbl-wrap"><table>'
+'<thead><tr><th>Timestamp</th><th>User</th><th>Action</th><th>Entity</th><th>Name</th><th>Details</th></tr></thead>'
+'<tbody>'+(rows||'<tr><td colspan="6" style="text-align:center;padding:26px;color:var(--g400)">No logs yet.</td></tr>')+'</tbody>'
+'</table></div></div></div>';
}
function exportLogs(){
var logs=[].concat(DB.logs).reverse();
var hdr='Timestamp,User,Action,Entity Type,Entity Name,Details\n';
var rows=logs.map(function(l){var u=DB.users.find(function(x){return x.id===l.userId;});return [l.timestamp.slice(0,19).replace('T',' '),u?u.username:'System',l.action,l.entityType||'',l.entityName||'',l.details||''].map(function(v){return '"'+String(v).replace(/"/g,'""')+'"';}).join(',');}).join('\n');
var a=document.createElement('a');a.href=URL.createObjectURL(new Blob([hdr+rows],{type:'text/csv'}));a.download='pinnacle_logs_'+today()+'.csv';a.click();
addLog('EXPORT_LOGS','system');saveDB();
}
/* ?? PROFILE ?? */
function renderProfile(){
var pref=getUserEmailPref(CU.id);
var notifEmail=pref.email||CU.email||'';
g('mainContent').innerHTML=
'<div style="max-width:620px">'
+'<div class="card"><div class="card-hdr"><span class="card-title"><i class="fas fa-user-circle tgold"></i> Account Settings</span></div><div class="card-body">'
+'<div class="fg-2"><div class="fg"><label>Full Name</label><input type="text" id="pFn" value="'+(CU.fullName||'')+'"></div><div class="fg"><label>Username</label><input type="text" value="'+CU.username+'" disabled style="background:var(--g50);color:var(--g400)"></div></div>'
+'<div class="fg-2"><div class="fg"><label>Email</label><input type="email" id="pEm" value="'+(CU.email||'')+'"></div><div class="fg"><label>Department</label><input type="text" id="pDe" value="'+(CU.dept||'')+'"></div></div>'
+'<div class="divider"></div>'
+'<div style="font-family:\'Playfair Display\',serif;font-size:14px;color:var(--navy);margin-bottom:10px">Change Password</div>'
+'<div class="fg-2"><div class="fg"><label>Current Password</label><input type="password" id="pCp"></div><div class="fg"><label>New Password</label><input type="password" id="pNp"></div></div>'
+'<div class="flex gap-2"><button class="btn btn-primary" onclick="saveProfile()"><i class="fas fa-save"></i> Save Changes</button><button class="btn btn-outline" onclick="showPage(\'dashboard\')">Cancel</button></div>'
+'</div></div>'
// Email notification preferences
+'<div class="card"><div class="card-hdr"><span class="card-title"><i class="fas fa-envelope tgold"></i> Email Notifications</span></div><div class="card-body">'
+'<p style="font-size:13px;color:var(--g600);margin-bottom:14px">Receive an email whenever any user makes a change to the dashboard. Requires EmailJS to be configured by an admin.</p>'
+'<div style="background:var(--g50);border:1px solid var(--g200);border-radius:8px;padding:14px;margin-bottom:14px">'
+'<div class="email-toggle" style="margin-bottom:12px">'
+'<label class="toggle-switch"><input type="checkbox" id="emailToggle" '+(pref.enabled?'checked':'')+' onchange="previewEmailToggle()"><span class="toggle-slider"></span></label>'
+'<div><div style="font-size:13px;font-weight:600;color:var(--navy)">Email alerts for dashboard changes</div>'
+'<div style="font-size:11px;color:var(--g400);margin-top:2px">You will be notified when others update data</div></div>'
+'</div>'
+'<div class="fg" style="margin-bottom:6px"><label>Notification Email Address</label>'
+'<input type="email" id="notifEmail" placeholder="your@email.com" value="'+notifEmail+'">'
+'<div style="font-size:11px;color:var(--g400);margin-top:4px">Leave blank to use your account email ('+CU.email+')</div>'
+'</div>'
+'</div>'
+'<div style="display:flex;gap:8px;flex-wrap:wrap">'
+'<button class="btn btn-primary" onclick="saveEmailPrefs()"><i class="fas fa-save"></i> Save Notification Prefs</button>'
+'<button class="btn btn-outline btn-sm" onclick="testEmailNotif()" id="testEmailBtn" style="margin-left:auto"><i class="fas fa-paper-plane"></i> Send Test Email</button>'
+'</div>'
+'</div></div>';
}
function previewEmailToggle(){
var on=document.getElementById('emailToggle')&&document.getElementById('emailToggle').checked;
// visual only
}
function saveEmailPrefs(){
var enabled=document.getElementById('emailToggle')&&document.getElementById('emailToggle').checked;
var email=(document.getElementById('notifEmail')||{}).value||'';
setUserEmailPref(CU.id,enabled,email);
flash(enabled?'Email notifications enabled. You will receive alerts when changes are made.':'Email notifications disabled.');
}
function testEmailNotif(){
var pk=DB._ejPk||''; var svc=DB._ejSvc||''; var tpl=DB._ejTpl||'';
if(!pk||!svc||!tpl){flash('EmailJS is not configured yet. Click the ☁ button in the topbar → fill in Step 2.','danger');return;}
if(!window.emailjs){flash('EmailJS library not loaded. Check your internet connection.','danger');return;}
var pref=getUserEmailPref(CU.id);
var emailAddr=(pref.email||CU.email||'').trim();
if(!emailAddr||emailAddr.indexOf('@')<0){flash('Please enter a valid notification email address below first.','danger');return;}
var btn=document.getElementById('testEmailBtn');
if(btn){btn.disabled=true;btn.innerHTML='<i class="fas fa-spinner fa-spin"></i> Sending...';}
try{emailjs.init({publicKey:pk});}catch(e){}
emailjs.send(svc, tpl, {
to_name: CU.fullName||CU.username,
to_email: emailAddr,
from_name: 'Pinnacle Decision Portal',
reply_to: emailAddr,
changed_by: 'Test System',
change_description: 'This is a test notification from the Pinnacle Decision Portal. If you received this, email notifications are working correctly.',
portal_url: window.location.href,
timestamp: new Date().toLocaleString()
}).then(function(){
flash('<i class="fas fa-check-circle"></i> Test email sent to '+emailAddr+' — check your inbox!','success');
if(btn){btn.disabled=false;btn.innerHTML='<i class="fas fa-paper-plane"></i> Send Test Email';}
}).catch(function(e){
var msg=e.text||e.message||JSON.stringify(e);
flash('Email failed: '+msg+' — double-check your Service ID, Template ID, and that the template uses {{to_email}} as the recipient.','danger');
if(btn){btn.disabled=false;btn.innerHTML='<i class="fas fa-paper-plane"></i> Send Test Email';}
});
}
function saveProfile(){
var cp=g('pCp').value,np=g('pNp').value;
if(np){if(cp!==CU.password){flash('Current password incorrect.','danger');return;}CU.password=np;}
CU.fullName=g('pFn').value;CU.email=g('pEm').value;CU.dept=g('pDe').value;
Object.assign(DB.users.find(function(x){return x.id===CU.id;}),CU);
addLog('UPDATE_PROFILE','user',CU.id,CU.username);saveDB();
g('userAv').textContent=(CU.fullName||CU.username)[0].toUpperCase();
g('userName').textContent=CU.fullName||CU.username;
flash('Profile updated.');renderProfile();
}
/* ?? IMPORT / EXPORT ?? */
function openIE(){
if(!g('ieModal')){
var el=document.createElement('div');
el.innerHTML='<div class="overlay open" id="ieModal"><div class="modal" style="max-width:520px">'
+'<div class="modal-hdr"><span class="modal-title"><i class="fas fa-exchange-alt"></i> Import / Export Data</span><button class="modal-close" onclick="closeModal(\'ieModal\')">&#x2715;</button></div>'
+'<div class="modal-body">'
+'<div style="background:var(--ibg);border:1px solid #bfdbfe;border-radius:8px;padding:14px;margin-bottom:18px;font-size:13px;color:var(--info)">'
+'<strong><i class="fas fa-info-circle"></i> How to share with your boss:</strong><br><br>'
+'<b>Step 1:</b> Click <em>Export Data</em> &mdash; saves a .json file<br>'
+'<b>Step 2:</b> Email <b>both</b> the HTML portal file <b>and</b> the .json file<br>'
+'<b>Step 3:</b> Boss opens the HTML, clicks the &#8644; button, imports the .json &mdash; sees all your data<br>'
+'<b>Step 4:</b> Boss makes updates, exports their .json, emails it back<br>'
+'<b>Step 5:</b> You import it &mdash; portal updates with all changes</div>'
+'<div style="border:1px solid var(--g200);border-radius:8px;padding:16px;margin-bottom:14px">'
+'<div style="font-family:\'Playfair Display\',serif;font-size:14px;color:var(--navy);margin-bottom:6px"><i class="fas fa-file-export tgold"></i> Export Current Data</div>'
+'<p style="font-size:13px;color:var(--g600);margin-bottom:12px">Downloads all data as a single .json file.</p>'
+'<button class="btn btn-primary" onclick="doExport()"><i class="fas fa-download"></i> Export Data as JSON</button></div>'
+'<div style="border:1px solid var(--g200);border-radius:8px;padding:16px">'
+'<div style="font-family:\'Playfair Display\',serif;font-size:14px;color:var(--navy);margin-bottom:6px"><i class="fas fa-file-import tgold"></i> Import Data</div>'
+'<p style="font-size:13px;color:var(--g600);margin-bottom:12px">Select a Pinnacle export .json file. <strong style="color:var(--danger)">This will replace all current data.</strong></p>'
+'<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">'
+'<input type="file" id="ieFile" accept=".json" style="font-size:13px;flex:1;min-width:200px;border:1px solid var(--g200);border-radius:6px;padding:6px 10px">'
+'<button class="btn btn-gold" onclick="doImport()"><i class="fas fa-upload"></i> Import</button></div></div>'
+'</div><div class="modal-foot"><button class="btn btn-outline" onclick="closeModal(\'ieModal\')">Close</button></div>'
+'</div></div>';
document.body.appendChild(el.firstElementChild);
} else { openModal('ieModal'); }
}
function doExport(){
var payload={exportedAt:new Date().toISOString(),exportedBy:CU.fullName||CU.username,version:'pinnacle_v3',data:DB};
var a=document.createElement('a');a.href=URL.createObjectURL(new Blob([JSON.stringify(payload,null,2)],{type:'application/json'}));
a.download='Pinnacle_Data_'+today()+'.json';a.click();
addLog('EXPORT_DATA','system');saveDB();flash('Data exported successfully.');
}
function doImport(){
var fi=g('ieFile');var file=fi?fi.files[0]:null;
if(!file){alert('Please select a .json file first.');return;}
var r=new FileReader();
r.onload=function(e){
try{
var p=JSON.parse(e.target.result);
var incoming=p.data||p;
if(!incoming.users||!incoming.phases||!incoming.milestones){alert('Invalid file. Please select a Pinnacle export file.');return;}
if(!confirm('Replace ALL current data with the imported data?'))return;
DB=incoming;
if(!DB.ids)DB.ids={user:999,phase:999,milestone:999,activity:999,deliverable:999,decision:999,risk:999,comment:999,log:999,notification:999};
saveDB();
var mu=DB.users.find(function(u){return u.username===CU.username;});
if(mu){CU=mu;g('userAv').textContent=(CU.fullName||CU.username)[0].toUpperCase();g('userName').textContent=CU.fullName||CU.username;g('userRole').textContent=CU.role;g('adminNav').style.display=CU.role==='admin'?'':'none';}
closeModal('ieModal');showPage('dashboard');flash('Data imported successfully!');
}catch(err){alert('Failed to read file.\n'+err.message);}
};r.readAsText(file);
}
/* ?? INIT ?? */
loadDB(); initEmailJS(); updateSyncUI();
/* ── AUTO-CONNECT JSONBIN ──
Replace the two values below with your NEW credentials
after regenerating them on jsonbin.io
── ────────────────────── */
(function(){
var BIN_ID = DB._jbBinId || ''; // set via Settings → ☁ button
var API_KEY = DB._jbApiKey || ''; // set via Settings → ☁ button
if(BIN_ID && API_KEY){
_JB.binId = BIN_ID;
_JB.apiKey = API_KEY;
_JB.enabled = true;
jbSetStatus('live','Synced');
jbStartPolling();
updateSyncUI();
console.log('JSONBin auto-connected from saved config.');
}
})();
if(window.innerWidth<900)g('mobBtn').style.display='flex';
/* ?? GANTT CHART ? ENHANCED INTERACTIVE ?? */
/* Global date resolver — used by Gantt, Executive Summary, and Upcoming panels */
function resolveMilDate(m, projStart){
var ps = projStart || (DB.projectStart ? new Date(DB.projectStart) : new Date());
ps = new Date(ps.getFullYear(), ps.getMonth(), 1);
var sd = m.tentDate ? new Date(m.tentDate) : (m.startDate ? new Date(m.startDate) : null);
var ed = m.endDate ? new Date(m.endDate) : null;
if(!sd && !ed && m.month){
var parts = String(m.month).split('-').map(function(p){return parseInt(p.trim(),10);});
var sm2 = parts[0]||1;
var em2 = parts[parts.length-1]||sm2;
sd = new Date(ps.getFullYear(), ps.getMonth()+sm2-1, 1);
ed = new Date(ps.getFullYear(), ps.getMonth()+em2, 0);
}
if(sd && !ed) ed = sd;
if(!sd && ed) sd = ed;
return {sd:sd, ed:ed};
}
var _ganttTooltip=null;
function renderGantt(phases){
var box=document.getElementById('ganttBox');
if(!box)return;
// Collect all milestones
var allM=[];
phases.forEach(function(p){
DB.milestones.filter(function(m){return m.phaseId===p.id;}).forEach(function(m){
allM.push({phase:p,milestone:m});
});
});
if(!allM.length){box.innerHTML='<div class="empty" style="padding:30px"><i class="fas fa-stream"></i><p>Add milestones with dates to see the Gantt chart.</p></div>';return;}
// Resolve dates — use tentDate/endDate if set, otherwise derive from month field
var projStart = DB.projectStart ? new Date(DB.projectStart) : new Date();
projStart = new Date(projStart.getFullYear(), projStart.getMonth(), 1);
function monthFieldToDates(monthStr){
if(!monthStr) return {s:null,e:null};
var parts = String(monthStr).split('-').map(function(p){return parseInt(p.trim(),10);});
var startM = parts[0]||1;
var endM = parts[parts.length-1]||startM;
var s = new Date(projStart.getFullYear(), projStart.getMonth()+startM-1, 1);
var e = new Date(projStart.getFullYear(), projStart.getMonth()+endM, 0);
return {s:s, e:e};
}
function resolveDate(m){
return resolveMilDate(m, projStart);
}
var dates=[];
allM.forEach(function(x){
var r=resolveDate(x.milestone);
if(r.sd) dates.push(r.sd);
if(r.ed) dates.push(r.ed);
});
if(!dates.length){box.innerHTML='<div class="empty" style="padding:30px"><i class="fas fa-stream"></i><p>Set milestone dates or a project start date to see the Gantt chart.</p></div>';return;}
// Timeline bounds — snap to month boundaries
var minRaw=new Date(Math.min.apply(null,dates));
var maxRaw=new Date(Math.max.apply(null,dates));
var minD=new Date(minRaw.getFullYear(),minRaw.getMonth(),1); // start of first month
var maxD=new Date(maxRaw.getFullYear(),maxRaw.getMonth()+1,0); // end of last month
var totalMs=maxD-minD;
var totalDays=totalMs/(1000*60*60*24);
// Colours
// Phase colour palette — rich professional colours
var pColors=[
['#6366f1','#4f46e5','ph-pill-1'], // indigo
['#0ea5e9','#0284c7','ph-pill-2'], // sky
['#10b981','#059669','ph-pill-3'], // emerald
['#f59e0b','#d97706','ph-pill-4'], // amber
['#ec4899','#db2777','ph-pill-5'], // pink
['#8b5cf6','#7c3aed','ph-pill-6'], // violet
];
var statusColors={
completed: ['#10b981','#059669'],
delayed: ['#f43f5e','#e11d48'],
in_progress:['#6366f1','#4f46e5'],
not_started:['#94a3b8','#64748b']
};
// Build month list — each month covers exactly its calendar days
var months=[];
var cur=new Date(minD.getFullYear(),minD.getMonth(),1);
while(cur<=maxD){
var mEnd=new Date(cur.getFullYear(),cur.getMonth()+1,0); // last day of month
var days=(mEnd-cur)/(1000*60*60*24)+1;
months.push({
label: cur.toLocaleString('default',{month:'long',year:'numeric'}),
shortLabel: cur.toLocaleString('default',{month:'short',year:'2-digit'}),
start: new Date(cur),
end: new Date(cur.getFullYear(),cur.getMonth()+1,0,23,59,59),
days: days,
pct: days/totalDays*100
});
cur.setMonth(cur.getMonth()+1);
}
// Today metrics
var now=new Date();
var todayPct=Math.max(0,Math.min(100,(now-minD)/totalMs*100));
var nowInRange=now>=minD&&now<=maxD;
var todayStr=now.toLocaleDateString('default',{weekday:'short',day:'numeric',month:'long',year:'numeric'});
// Which month is current?
var currentMonthIdx=-1;
months.forEach(function(m,i){if(now>=m.start&&now<=m.end)currentMonthIdx=i;});
// Which milestone is currently active (in_progress and today within its range)?
var activeMillId=-1;
allM.forEach(function(x){
var m=x.milestone;
var s=m.tentDate?new Date(m.tentDate):null;
var e=m.endDate?new Date(m.endDate):null;
if(s&&e&&now>=s&&now<=e&&m.status!=='completed'){activeMillId=m.id;}
});
// Phase index
var phaseIdx={};
phases.forEach(function(p,i){phaseIdx[p.id]=i;});
// Tooltip element
if(!_ganttTooltip){
_ganttTooltip=document.createElement('div');
_ganttTooltip.className='gantt-tooltip';
document.body.appendChild(_ganttTooltip);
}
// ── HEADER — month columns ──
var hdr='<div class="gantt-header"><div class="gantt-label-col">Milestone</div><div class="gantt-months" style="position:relative">';
months.forEach(function(m,i){
var isNow=i===currentMonthIdx;
hdr+='<div class="gantt-month'+(isNow?' gantt-month-active':'')+'" style="width:'+m.pct+'%;flex-shrink:0">'+m.shortLabel+'</div>';
});
// "NOW" pointer in header track
if(nowInRange){
hdr+='<div class="gantt-now-pointer" style="left:'+todayPct+'%">'
+'<div class="gantt-now-label">TODAY</div>'
+'<div class="gantt-now-arrow"></div>'
+'</div>';
}
hdr+='</div></div>';
// ── ROWS ──
var rows='<div class="gantt-body" style="position:relative">';
// Full-height NOW line across entire body (rendered once as overlay)
if(nowInRange){
rows+='<div class="gantt-now-line" style="left:calc(220px + '+(todayPct*(100)/100)+'% * (100% - 220px) / 100)" id="ganttNowLine"></div>';
}
allM.forEach(function(x,ri){
var m=x.milestone,p=x.phase;
var _pc=pColors[phaseIdx[p.id]%pColors.length];
var col=_pc[0], colDark=_pc[1], colPill=_pc[2];
var _rd=resolveDate(m);
var sDate=_rd.sd, eDate=_rd.ed;
var pct=milestonePct(m.id);
var _sc=statusColors[m.status]||[col,colDark];
var barCol=_sc[0], barColDark=_sc[1];
var labelShort=m.title.length>26?m.title.slice(0,24)+'…':m.title;
var isActiveRow=m.id===activeMillId;
rows+='<div class="gantt-row'+(isActiveRow?' gantt-active-row':'')+'" id="gr-'+m.id+'">'
+'<div class="gantt-row-label"><div class="mil-name" title="'+m.title+'">'+labelShort+'</div>'
+'<div class="ph-tag '+colPill+'">Phase '+p.num+'</div></div>'
+'<div class="gantt-track">';
// Per-row today line
if(nowInRange){
rows+='<div style="position:absolute;left:'+todayPct+'%;top:0;width:2px;height:100%;z-index:3;pointer-events:none;background:linear-gradient(180deg,rgba(239,68,68,.7),rgba(239,68,68,.1))"></div>';
}
if(sDate&&eDate){
var barLeft=((sDate-minD)/totalMs)*100;
var barWidth=Math.max(1.5,((eDate-sDate+86400000)/totalMs)*100);
barLeft=Math.max(0,Math.min(99,barLeft));
barWidth=Math.min(100-barLeft,barWidth);
var sStr=sDate.toLocaleDateString('default',{month:'short',day:'numeric',year:'numeric'});
var eStr=eDate.toLocaleDateString('default',{month:'short',day:'numeric',year:'numeric'});
var statusLabel=(m.status||'not_started').replace(/_/g,' ').replace(/\b\w/g,function(c){return c.toUpperCase();});
var tooltipContent='<strong>'+m.title+'</strong>'
+(isActiveRow?'<span style="background:#ef4444;color:#fff;font-size:9px;font-weight:800;padding:1px 5px;border-radius:3px;margin-left:6px">ACTIVE</span>':'')
+'<br><span style="opacity:.7;font-size:11px">'+sStr+' → '+eStr+'</span><br>'
+'<span style="font-size:11px;margin-top:4px;display:block">Status: <b>'+statusLabel+'</b> &nbsp;|&nbsp; Progress: <b>'+pct+'%</b></span>';
rows+='<div class="gantt-bar" style="left:'+barLeft+'%;width:'+barWidth+'%;background:linear-gradient(90deg,'+barCol+','+barCol+'cc);'
+(isActiveRow?'box-shadow:0 0 0 2px rgba(201,162,39,.6),0 2px 8px rgba(0,0,0,.25);':'')
+'"'
+' onmouseenter="showGanttTip(event,\''+tooltipContent.replace(/'/g,'&#39;').replace(/"/g,'&quot;')+'\')" onmouseleave="hideGanttTip()"'
+' onclick="showPage(\'milestoneDetail\','+m.id+')">'
+'<div class="gantt-bar-fill" style="width:'+pct+'%"></div>'
+'<span class="gantt-bar-pct">'+pct+'%</span>'
+'</div>';
// Pulsing dot on active milestone at today's position
if(isActiveRow&&nowInRange&&todayPct>=barLeft&&todayPct<=barLeft+barWidth){
rows+='<div class="gantt-pulse-dot" style="left:'+todayPct+'%"></div>';
}
} else {
rows+='<div style="position:absolute;left:2%;font-size:10px;color:var(--g400);line-height:42px">No dates set</div>';
}
rows+='</div></div>';
});
rows+='</div>';
// ── LEGEND ──
var legend='<div class="gantt-legend">'
+'<div class="gantt-legend-item"><div class="gantt-legend-dot" style="background:linear-gradient(135deg,#10b981,#059669)"></div>Completed</div>'
+'<div class="gantt-legend-item"><div class="gantt-legend-dot" style="background:linear-gradient(135deg,#6366f1,#4f46e5)"></div>In Progress</div>'
+'<div class="gantt-legend-item"><div class="gantt-legend-dot" style="background:linear-gradient(135deg,#f43f5e,#e11d48)"></div>Delayed</div>'
+'<div class="gantt-legend-item"><div class="gantt-legend-dot" style="background:linear-gradient(135deg,#94a3b8,#64748b)"></div>Not Started</div>'
+'<div class="gantt-legend-item"><div style="width:12px;height:12px;border-radius:50%;background:#f43f5e;border:2px solid #fff;box-shadow:0 0 0 2px rgba(244,63,94,.3);animation:gantt-pulse 1.8s infinite"></div>Active Now</div>'
+'<div style="margin-left:auto;font-size:11px;color:#64748b;font-weight:600;display:flex;align-items:center;gap:6px">'
+'<div style="width:2px;height:16px;background:linear-gradient(180deg,#f43f5e,rgba(244,63,94,.3));border-radius:1px"></div>'
+(nowInRange?todayStr:'Today outside project range')
+'</div>'
+'</div>';
box.innerHTML='<div class="gantt-wrap">'+hdr+rows+legend+'</div>';
}
function showGanttTip(e,html){
if(!_ganttTooltip)return;
_ganttTooltip.innerHTML=html;
_ganttTooltip.style.display='block';
_ganttTooltip.style.left=(e.clientX+14)+'px';
_ganttTooltip.style.top=(e.clientY-10)+'px';
// Boundary check
var rect=_ganttTooltip.getBoundingClientRect();
if(rect.right>window.innerWidth-10)_ganttTooltip.style.left=(e.clientX-rect.width-10)+'px';
}
function hideGanttTip(){if(_ganttTooltip)_ganttTooltip.style.display='none';}
/* ?? PROGRESS AUTO-CALC ?? */
function calcProgress(startDate, endDate, status){
if(status==='completed') return 100;
if(status==='not_started') return 0;
if(!startDate||!endDate) return 0;
var s=new Date(startDate), e=new Date(endDate), t=new Date();
if(t<=s) return 0;
if(t>=e) return status==='completed'?100:90;
var prog=Math.round(((t-s)/(e-s))*100);
return Math.min(99,Math.max(1,prog));
}
/* ?? COMMENT HELPERS ?? */
function editComment(cid,mid){
var textEl=document.getElementById('cmt-text-'+cid);
var editEl=document.getElementById('cmt-edit-'+cid);
if(textEl)textEl.style.display='none';
if(editEl)editEl.style.display='block';
var ta=document.getElementById('cmt-ta-'+cid);
if(ta)ta.focus();
}
function cancelEditComment(cid){
var textEl=document.getElementById('cmt-text-'+cid);
var editEl=document.getElementById('cmt-edit-'+cid);
if(textEl)textEl.style.display='block';
if(editEl)editEl.style.display='none';
}
function saveEditComment(cid,mid){
var ta=document.getElementById('cmt-ta-'+cid);
if(!ta)return;
var txt=ta.value.trim();
if(!txt){alert('Comment cannot be empty.');return;}
var c=DB.comments.find(function(x){return x.id===cid;});
if(!c)return;
c.content=txt;c.edited=true;c.editedAt=new Date().toISOString();
addLog('EDIT_COMMENT','comment',cid);saveDB();renderMilestoneDetail(mid);flash('Comment updated.');
}
function delComment(cid,mid){
if(!confirm('Delete this comment?'))return;
DB.comments=DB.comments.filter(function(x){return x.id!==cid;});
addLog('DELETE_COMMENT','comment',cid);saveDB();renderMilestoneDetail(mid);flash('Comment deleted.','danger');
}
/* ?? KPI CARD MODALS ?? */
function closeKpi(){var p=document.getElementById('kpiModal');if(p)p.remove();}
function showKpiModal(type){
var _types=['overall','completed','delayed','dueweek','decisions','risks'];
if(typeof type==='number')type=_types[type];
var old=document.getElementById('kpiModal');
if(old)old.remove();
var title='', content='', color='', icon='';
var td=today();
var in7=addDays(td,7);
if(type==='overall'){
title='Overall Project Completion';color='#c9a227';icon='fa-chart-pie';
var phases=DB.phases.slice().sort(function(a,b){return a.order-b.order;});
var rows=phases.map(function(p){
var pp=phasePct(p.id);
var mils=DB.milestones.filter(function(m){return m.phaseId===p.id;});
var done=mils.filter(function(m){var a=DB.activities.filter(function(x){return x.milestoneId===m.id;});return a.length&&a.every(function(x){return x.status==='completed';});}).length;
return '<tr><td style="font-weight:600">Phase '+p.num+': '+p.name+'</td>'
+'<td>'+mils.length+'</td><td>'+done+'</td>'
+'<td><div style="display:flex;align-items:center;gap:8px"><div class="prog-wrap" style="flex:1;height:6px"><div class="prog-bar '+(pp>=100?'ps':'')+'" style="width:'+pp+'%"></div></div><span style="font-size:12px;font-weight:700;min-width:35px">'+pp+'%</span></div></td>'
+'<td>'+sBadge(mils.length&&mils.every(function(m){return m.status==='completed';})&&mils.length>0?'completed':'in_progress')+'</td></tr>';
}).join('');
var total=DB.activities.length, done2=DB.activities.filter(function(a){return a.status==='completed';}).length;
content='<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:18px">'
+'<div style="text-align:center;padding:12px;background:#f5e9c0;border-radius:8px"><div style="font-size:24px;font-weight:700;color:#c9a227">'+pct(total,done2)+'%</div><div style="font-size:11px;color:#b45309">Overall</div></div>'
+'<div style="text-align:center;padding:12px;background:#dcfce7;border-radius:8px"><div style="font-size:24px;font-weight:700;color:#15803d">'+done2+'</div><div style="font-size:11px;color:#15803d">Completed</div></div>'
+'<div style="text-align:center;padding:12px;background:#f4f4f5;border-radius:8px"><div style="font-size:24px;font-weight:700;color:#52525b">'+(total-done2)+'</div><div style="font-size:11px;color:#52525b">Remaining</div></div>'
+'</div>'
+'<div class="tbl-wrap"><table><thead><tr><th>Phase</th><th>Milestones</th><th>Done</th><th>Progress</th><th>Status</th></tr></thead><tbody>'+rows+'</tbody></table></div>';
}
else if(type==='completed'){
title='Completed Activities';color='#15803d';icon='fa-check-double';
var acts=DB.activities.filter(function(a){return a.status==='completed';});
var rows=acts.map(function(a){
var m=DB.milestones.find(function(x){return x.id===a.milestoneId;});
var p=m?DB.phases.find(function(x){return x.id===m.phaseId;}):null;
return '<tr>'
+'<td style="font-weight:600">'+a.title+'</td>'
+'<td class="fs12">'+(m?m.title.slice(0,30)+(m.title.length>30?'...':''):'—')+'</td>'
+'<td>'+(p?'<span class="badge b-navy" style="font-size:10px">Ph.'+p.num+'</span>':'—')+'</td>'
+'<td class="fs12">'+(a.endDate||'—')+'</td>'
+'<td>'+sBadge('completed')+'</td></tr>';
}).join('');
content=(acts.length?'<div class="tbl-wrap"><table><thead><tr><th>Activity</th><th>Milestone</th><th>Phase</th><th>End Date</th><th>Status</th></tr></thead><tbody>'+rows+'</tbody></table></div>'
:'<div class="empty"><i class="fas fa-check-double"></i><p>No completed activities yet.</p></div>');
}
else if(type==='delayed'){
title='Delayed Tasks';color='#b91c1c';icon='fa-clock';
var acts=DB.activities.filter(function(a){return a.endDate&&a.endDate<td&&a.status!=='completed';});
var rows=acts.map(function(a){
var m=DB.milestones.find(function(x){return x.id===a.milestoneId;});
var p=m?DB.phases.find(function(x){return x.id===m.phaseId;}):null;
var daysLate=Math.round((new Date(td)-new Date(a.endDate))/(1000*60*60*24));
return '<tr>'
+'<td style="font-weight:600">'+a.title+'</td>'
+'<td class="fs12">'+(m?m.title.slice(0,28)+(m.title.length>28?'...':''):'—')+'</td>'
+'<td>'+(p?'<span class="badge b-navy" style="font-size:10px">Ph.'+p.num+'</span>':'—')+'</td>'
+'<td><span style="color:#b91c1c;font-weight:700;font-size:13px">'+a.endDate+'</span></td>'
+'<td><span style="color:#b91c1c;font-size:12px;font-weight:600">'+daysLate+' day'+(daysLate!==1?'s':'')+' overdue</span></td>'
+'<td>'+sBadge(a.status)+'</td>'
+'<td>'+(isEditor()?'<button class="btn btn-outline btn-xs" onclick="closeKpi();showPage(\'phaseDetail\',' +(p?p.id:0)+ ')" >View</button>':'')+'</td></tr>';
}).join('');
content=(acts.length?'<div class="tbl-wrap"><table><thead><tr><th>Activity</th><th>Milestone</th><th>Phase</th><th>Due Date</th><th>Overdue By</th><th>Status</th><th></th></tr></thead><tbody>'+rows+'</tbody></table></div>'
:'<div class="empty"><i class="fas fa-clock"></i><p>No delayed tasks. Great work!</p></div>');
}
else if(type==='dueweek'){
title='Due This Week';color='#d97706';icon='fa-hourglass-half';
var acts=DB.activities.filter(function(a){return a.endDate&&a.endDate>=td&&a.endDate<=in7&&a.status!=='completed';});
var rows=acts.map(function(a){
var m=DB.milestones.find(function(x){return x.id===a.milestoneId;});
var p=m?DB.phases.find(function(x){return x.id===m.phaseId;}):null;
var daysLeft=Math.round((new Date(a.endDate)-new Date(td))/(1000*60*60*24));
var urgColor=daysLeft<=1?'#b91c1c':daysLeft<=3?'#d97706':'#15803d';
return '<tr>'
+'<td style="font-weight:600">'+a.title+'</td>'
+'<td class="fs12">'+(m?m.title.slice(0,28)+(m.title.length>28?'...':''):'—')+'</td>'
+'<td>'+(p?'<span class="badge b-navy" style="font-size:10px">Ph.'+p.num+'</span>':'—')+'</td>'
+'<td><span style="color:'+urgColor+';font-weight:700;font-size:13px">'+a.endDate+'</span></td>'
+'<td><span style="color:'+urgColor+';font-size:12px;font-weight:600">'+(daysLeft===0?'Today':daysLeft===1?'Tomorrow':daysLeft+' days')+'</span></td>'
+'<td>'+sBadge(a.status)+'</td>'
+'<td>'+(isEditor()?'<button class="btn btn-outline btn-xs" onclick="closeKpi();openAM(' +a.milestoneId+ ',' +a.id+ ')" >Edit</button>':'')+'</td></tr>';
}).join('');
content=(acts.length?'<div class="tbl-wrap"><table><thead><tr><th>Activity</th><th>Milestone</th><th>Phase</th><th>Due Date</th><th>Urgency</th><th>Status</th><th></th></tr></thead><tbody>'+rows+'</tbody></table></div>'
:'<div class="empty"><i class="fas fa-hourglass-half"></i><p>Nothing due in the next 7 days.</p></div>');
}
else if(type==='decisions'){
title='Pending Decisions';color='#1d4ed8';icon='fa-gavel';
var decs=DB.decisions.filter(function(d){return d.status==='pending'||d.status==='in_review';});
var rows=decs.map(function(d){
var u=DB.users.find(function(x){return x.id===d.raisedBy;});
var p=DB.phases.find(function(x){return x.id===d.phaseId;});
var overdue=d.dueDate&&d.dueDate<td;
return '<tr>'
+'<td><div style="font-weight:600">'+d.title+'</div>'+(d.description?'<div class="muted fs12">'+d.description.slice(0,60)+(d.description.length>60?'...':'')+'</div>':'')+'</td>'
+'<td>'+(p?'<span class="badge b-navy" style="font-size:10px">Ph.'+p.num+'</span>':'—')+'</td>'
+'<td>'+sBadge(d.status)+'</td>'
+'<td>'+pBadge(d.priority)+'</td>'
+'<td class="fs12">'+(d.raisedBy?(u?u.fullName||u.username:'—'):'—')+'</td>'
+'<td><span style="'+(overdue?'color:#b91c1c;font-weight:700;':'')+' font-size:12px">'+(d.dueDate||'—')+(overdue?' ⚠':'')+'</span></td>'
+'<td>'+(d.fileData?'<button class="btn btn-outline btn-xs" onclick="dlDecFile('+d.id+')" title="Download document"><i class="fas fa-file-download"></i></button>':'')+'</td></tr>';
}).join('');
content=(decs.length?'<div class="tbl-wrap"><table><thead><tr><th>Decision</th><th>Phase</th><th>Status</th><th>Priority</th><th>Raised By</th><th>Due Date</th><th>Doc</th></tr></thead><tbody>'+rows+'</tbody></table></div>'
:'<div class="empty"><i class="fas fa-gavel"></i><p>No pending decisions.</p></div>');
}
else if(type==='risks'){
title='Open Risks';color='#7c3aed';icon='fa-shield-halved';
var risks=DB.risks.filter(function(r){return r.status==='open'||r.status==='in_review';});
var pb={low:'b-gray',medium:'b-warn',high:'b-danger'};
var rows=risks.map(function(r){
var p=DB.phases.find(function(x){return x.id===r.phaseId;});
var heat=(r.probability==='high'&&r.impact==='high')?'critical':(r.probability==='high'||r.impact==='high')?'high':'medium';
return '<tr>'
+'<td><div style="font-weight:600">'+r.title+'</div>'+(r.mitigation?'<div class="fs12 mt-1" style="color:#15803d"><i class="fas fa-shield-halved"></i> '+r.mitigation.slice(0,50)+(r.mitigation.length>50?'...':'')+'</div>':'')+'</td>'
+'<td><span class="badge '+(pb[r.probability]||'b-gray')+'">'+(r.probability||'—')+'</span></td>'
+'<td><span class="badge '+(pb[r.impact]||'b-gray')+'">'+(r.impact||'—')+'</span></td>'
+'<td><span class="badge '+(heat==='critical'?'b-danger':heat==='high'?'b-warn':'b-gray')+'">'+heat+'</span></td>'
+'<td>'+sBadge(r.status)+'</td>'
+'<td class="fs12">'+(r.owner||'—')+'</td>'
+'<td>'+(p?'<span class="badge b-navy" style="font-size:10px">Ph.'+p.num+'</span>':'—')+'</td></tr>';
}).join('');
content=(risks.length?'<div class="tbl-wrap"><table><thead><tr><th>Risk</th><th>Probability</th><th>Impact</th><th>Heat</th><th>Status</th><th>Owner</th><th>Phase</th></tr></thead><tbody>'+rows+'</tbody></table></div>'
:'<div class="empty"><i class="fas fa-shield-halved"></i><p>No open risks.</p></div>');
}
// Build modal using DOM to avoid quote issues
var overlay=document.createElement('div');
overlay.className='overlay open';
overlay.id='kpiModal';
var modal=document.createElement('div');
modal.className='modal';
modal.style.maxWidth='760px';
// Header
var hdr=document.createElement('div');
hdr.className='modal-hdr';
hdr.style.borderBottom='3px solid '+color;
hdr.innerHTML='<span class="modal-title"><i class="fas '+icon+'" style="margin-right:8px;color:'+color+'"></i>'+title+'</span>';
var xb=document.createElement('button');xb.className='modal-close';xb.innerHTML='&#x2715;';
xb.onclick=function(){var p=document.getElementById('kpiModal');if(p)p.remove();};
hdr.appendChild(xb);modal.appendChild(hdr);
// Body
var body=document.createElement('div');body.className='modal-body';body.innerHTML=content;
modal.appendChild(body);
// Footer
var foot=document.createElement('div');foot.className='modal-foot';
var cls=document.createElement('button');cls.className='btn btn-outline';cls.textContent='Close';
cls.onclick=function(){var p=document.getElementById('kpiModal');if(p)p.remove();};foot.appendChild(cls);
modal.appendChild(foot);
overlay.appendChild(modal);
document.body.appendChild(overlay);
overlay.addEventListener('click',function(e){if(e.target===overlay)overlay.remove();});
}
</script>
</body>
</html>