Kaliman-1981 commited on
Commit
3a8d013
·
verified ·
1 Parent(s): e937347

reset button needs to clear all..

Browse files
Files changed (2) hide show
  1. README.md +9 -5
  2. index.html +1472 -19
README.md CHANGED
@@ -1,10 +1,14 @@
1
  ---
2
- title: Costmaster Pro
3
- emoji: 🐨
4
- colorFrom: yellow
5
- colorTo: indigo
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
  ---
2
+ title: CostMaster Pro 🔥
3
+ colorFrom: blue
4
+ colorTo: green
5
+ emoji: 🐳
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite-v3
10
  ---
11
 
12
+ # Welcome to your new DeepSite project!
13
+ This project was created with [DeepSite](https://deepsite.hf.co).
14
+
index.html CHANGED
@@ -1,19 +1,1472 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
6
+ <title>Cost Master – S‑Curve</title>
7
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 3v18h18' stroke='%239ab0d8' fill='none'/%3E%3Cpath d='M6 15l4-5 3 3 5-7' stroke='%239ab0d8' fill='none'/%3E%3C/svg%3E"/>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script>
11
+ <script src="https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js"></script>
12
+ <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
13
+ <style>
14
+ :root{--bg:#0b1222;--panel:#0e1a32;--text:#e7eefc;--muted:#a3b5d9;--border:#233255;--teal:#22e1e8;--blue:#7ad7ff;--green:#86efac;--amber:#ffc455;--purple:#a87fff;--pink:#f08ac3;--orange:#ffa055;}
15
+ *{box-sizing:border-box}
16
+ body{background:var(--bg);color:var(--text);font-family:Inter,system-ui,sans-serif;margin:0}
17
+ .wrap{max-width:1280px;margin:0 auto}
18
+ header{position:sticky;top:0;z-index:10;background:rgba(11,18,34,.92);backdrop-filter:blur(6px);border-bottom:1px solid var(--border)}
19
+ .brand{display:flex;align-items:flex-end;gap:.55rem;padding:10px 0}
20
+ .brand .logo{width:32px;height:32px;border-radius:8px;background:#0f2042;border:1px solid #284475;display:flex;align-items:center;justify-content:center}
21
+ .brand .logo svg{width:20px;height:20px;stroke:#a9bfe6;fill:none;stroke-width:2}
22
+ .brand .title{font-size:30px;line-height:1;font-weight:700;background:linear-gradient(180deg,#e6ecff,#a0b5df);-webkit-background-clip:text;color:transparent}
23
+ .brand .subtitle{font-size:12px;color:#a6b9de;margin-top:2px}
24
+ .btn{padding:.45rem .7rem;border-radius:10px;border:1px solid var(--border);background:#0e1e3b;color:#e7eefc;font-weight:600;font-size:.9rem}
25
+ .btn.warning{background:#f5a524;color:#142036;border-color:#f0aa3a}
26
+ .btn.danger{background:#943535;border-color:#b24b4b}
27
+ .filename{font-size:.85rem;color:#c5d3f1;opacity:.85}
28
+ #file{position:absolute;left:-9999px;width:1px;height:1px;opacity:0}
29
+ .card{background:linear-gradient(180deg,rgba(255,255,255,.035),rgba(255,255,255,.015));border:1px solid var(--border);border-radius:14px;padding:1rem; margin-bottom: 1rem}
30
+ .section{display:flex;align-items:center;justify-content:space-between;margin:6px 0 8px}
31
+ .section h2{font-size:1rem;opacity:.95}
32
+ .hint{font-size:.8rem;color:var(--muted)}
33
+ /* KPI */
34
+ .kpi .label{color:var(--muted);font-size:.82rem;margin-bottom:.35rem;display:flex;gap:.45rem;align-items:center}
35
+ .kpi .value{font-weight:700;font-size:1.1rem;line-height:1.25;color:#dbe6ff;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;letter-spacing:.2px}
36
+ .kpi .value.small{font-size:1.02rem}
37
+ .kpi .note{margin-top:.25rem;font-size:.86rem;line-height:1.2;color:var(--muted);font-weight:500;letter-spacing:.15px}
38
+ .kpi-grid{display:grid;grid-template-columns:repeat(4, minmax(220px,1fr));gap:12px}
39
+ @media (max-width: 1100px){ .kpi-grid{grid-template-columns:repeat(auto-fit,minmax(240px,1fr));} }
40
+ /* Pills */
41
+ .grid-fit{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}
42
+ .badge{display:flex;align-items:center;gap:10px;background:#0f1e39;border:1px solid var(--border);padding:.45rem .7rem;border-radius:999px;min-width:220px}
43
+ .badge .dot{width:8px;height:8px;border-radius:999px;background:var(--teal)}
44
+ .badge .label{color:var(--muted);font-size:.9rem}
45
+ .badge .val{font-variant-numeric:tabular-nums;font-weight:700;white-space:nowrap}
46
+ /* Range slider */
47
+ .track{height:8px;background:#0f1e39;border:1px solid var(--border);border-radius:999px;position:relative}
48
+ .fill{position:absolute;height:8px;background:linear-gradient(90deg,#22e1e8,#7ad7ff);border-radius:999px}
49
+ .range{pointer-events:none;position:absolute;height:0;width:100%;-webkit-appearance:none}
50
+ .range::-webkit-slider-thumb{pointer-events:auto;width:14px;height:14px;border-radius:50%;background:var(--amber);-webkit-appearance:none;border:2px solid #13233f}
51
+ .chartbox{height:320px}
52
+ .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}
53
+
54
+ .compare-pill{display:inline-flex;align-items:center;gap:6px;padding:.2rem .45rem;border-radius:999px;border:1px solid var(--border);background:#0f1e39;font-size:.8rem}
55
+ .compare-dot{width:9px;height:9px;border-radius:999px;display:inline-block}
56
+ .compare-pill .val{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;opacity:.9}
57
+
58
+ .compare-legend-bottom{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:6px}
59
+
60
+ /* ---- S‑Curve Loader ---- */
61
+ .cm-loader{position:fixed;inset:0;background:rgba(9,14,28,.78);backdrop-filter:blur(6px);
62
+ display:flex;align-items:center;justify-content:center;z-index:70}
63
+ .cm-loader-card{width:min(780px,92vw);border:1px solid var(--border);border-radius:18px;
64
+ background:linear-gradient(180deg,rgba(255,255,255,.06),rgba(255,255,255,.02));
65
+ box-shadow:0 10px 40px rgba(0,0,0,.35); padding:18px 20px}
66
+ .cm-hero{width:100%;height:120px;display:block}
67
+ .cm-title{font-weight:700;letter-spacing:.2px}
68
+ .cm-sub{color:var(--muted);margin-top:2px}
69
+ .cm-loader-body{display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center}
70
+ .cm-progress{grid-column:1/2;height:10px;border-radius:999px;background:#0f1e39;border:1px solid var(--border);overflow:hidden;margin-top:6px}
71
+ .cm-bar{height:100%;width:0;background:linear-gradient(90deg,#22e1e8,#7ad7ff);transition:width .35s ease}
72
+ .cm-pct{grid-column:2/3;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
73
+ /* Curve draw + moving dot along path */
74
+ @keyframes cmDraw { from { stroke-dashoffset: 800; } to { stroke-dashoffset: 0; } }
75
+ @keyframes cmDash { from { stroke-dasharray: 1 10; } to { stroke-dasharray: 12 10; } }
76
+ @keyframes cmGlow { 0%,100% { filter: drop-shadow(0 0 0px #00e4ff);} 50% { filter: drop-shadow(0 0 6px #00e4ff);} }
77
+
78
+ /* --- loading toast --- */
79
+ .cm-toast{position:fixed;top:16px;left:50%;transform:translateX(-50%);z-index:80}
80
+ .cm-toast-card{display:flex;align-items:center;gap:8px;background:rgba(16,29,56,.92);
81
+ border:1px solid var(--border);padding:8px 12px;border-radius:10px;
82
+ box-shadow:0 6px 18px rgba(0,0,0,.35);color:#d9e7ff;font-weight:600}
83
+ .cm-dot{width:10px;height:10px;border-radius:50%;background:conic-gradient(#22e1e8,#7ad7ff);
84
+ animation: cmPulse 1s ease-in-out infinite alternate}
85
+ @keyframes cmPulse{from{filter:drop-shadow(0 0 0px #00e4ff)}to{filter:drop-shadow(0 0 6px #00e4ff)}}
86
+ </style>
87
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
88
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
89
+ </head>
90
+ <body>
91
+ <header class="px-6">
92
+ <div class="wrap flex items-center justify-between gap-3">
93
+ <div class="brand">
94
+ <div class="logo"><svg viewBox="0 0 24 24"><path d='M3 3v18h18'/><path d='M6 15l4-5 3 3 5-7'/></svg></div>
95
+ <div><div class="title">Cost Master</div><div class="subtitle">Advanced S‑Curve Analysis & Project Management</div></div>
96
+ </div>
97
+ <div class="flex items-center gap-2">
98
+ <label id="fileLabel" for="file" class="btn warning cursor-pointer" role="button" tabindex="0">+ Choose File</label>
99
+ <span id="filename" class="filename">No file selected</span>
100
+ <input id="file" type="file" accept=".xlsx,.xls,.xlsm,.csv,.XLSX,.XLS,.XLSM,.CSV"/>
101
+ <button id="exportCsv" class="btn">Export Daily CSV</button>
102
+ <button id="exportPdf" class="btn ml-2">Export Report PDF</button>
103
+ <button id="resetAll" class="btn danger">Reset</button>
104
+ </div>
105
+ </div>
106
+ </header>
107
+ <!-- Tiny "please wait" toast -->
108
+ <div id="cmToast" class="cm-toast" style="display:none;">
109
+ <div class="cm-toast-card">
110
+ <svg id="uploadSpinner" width="16" height="16" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#7ad7ff" style="display:none;">
111
+ <g fill="none" fill-rule="evenodd">
112
+ <g transform="translate(1 1)" stroke-width="2">
113
+ <circle stroke-opacity=".5" cx="18" cy="18" r="18"/>
114
+ <path d="M36 18c0-9.94-8.06-18-18-18">
115
+ <animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="1s" repeatCount="indefinite"/>
116
+ </path>
117
+ </g>
118
+ </g>
119
+ </svg>
120
+ <span class="cm-dot"></span>
121
+ <span id="cmToastMsg">Loading file… please wait</span>
122
+ </div>
123
+ </div>
124
+ <!-- S‑Curve Loader Overlay -->
125
+ <div id="cmLoader" class="cm-loader" style="display:none;">
126
+ <div class="cm-loader-card">
127
+ <svg class="cm-hero" viewBox="0 0 600 160" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
128
+ <!-- faint grid -->
129
+ <g opacity=".12">
130
+ <path d="M0 140 H600" stroke="#9fb4df"/>
131
+ <path d="M0 100 H600" stroke="#9fb4df"/>
132
+ <path d="M0 60 H600" stroke="#9fb4df"/>
133
+ <path d="M0 20 H600" stroke="#9fb4df"/>
134
+ </g>
135
+ <!-- S-curve path (animated stroke-dashoffset draw) -->
136
+ <path id="cmCurve" d="M10,140 C120,120 180,40 260,60 C330,77 360,95 410,80 C480,60 500,20 590,20"
137
+ fill="none" stroke="#7ad7ff" stroke-width="4" stroke-linecap="round" />
138
+ <!-- running dot -->
139
+ <circle id="cmDot" r="5" fill="#22e1e8"/>
140
+ </svg>
141
+ <div class="cm-loader-body">
142
+ <div class="cm-title">Loading data</div>
143
+ <div class="cm-sub" id="cmStep">Reading file…</div>
144
+ <div class="cm-progress">
145
+ <div id="cmBar" class="cm-bar"></div>
146
+ </div>
147
+ <div class="cm-pct" id="cmPct">0%</div>
148
+ </div>
149
+ </div>
150
+ </div>
151
+
152
+ <main class="wrap p-6 space-y-6">
153
+ <!-- OVERVIEW -->
154
+ <section>
155
+ <div class="section"><h2>Overview</h2><div class="hint">Rolling burn + quick forecast</div></div>
156
+ <div class="kpi-grid">
157
+ <div class="card"><div class="kpi"><div class="label">Total Cost</div><div id="k_total" class="value">—</div></div></div>
158
+ <div class="card"><div class="kpi"><div class="label">Date Range</div><div id="k_window" class="value">—</div></div></div>
159
+ <div class="card"><div class="kpi"><div class="label">Avg Daily Burn</div><div id="k_avg" class="value">—</div></div></div>
160
+ <div class="card"><div class="kpi"><div class="label">Forecast to Target</div>
161
+ <input id="targetDate" type="date" class="btn w-full my-1"/>
162
+ <div id="k_forecast" class="value small">—</div>
163
+ <div id="k_forecast_formula" class="note">—</div>
164
+ </div></div>
165
+ </div>
166
+ </section>
167
+
168
+ <!-- FILTERS -->
169
+ <section>
170
+ <div class="section"><h2>Filters</h2><div class="hint">Company search, MAFE/AFE, WBS & date slider</div></div>
171
+ <div class="card">
172
+ <div class="grid lg:grid-cols-3 gap-5">
173
+ <!-- Company -->
174
+ <div class="lg:col-span-2">
175
+ <div class="hint mb-1">Company</div>
176
+ <div class="card" style="padding:.6rem">
177
+ <div class="flex gap-2 mb-2">
178
+ <input id="search" class="btn w-full" placeholder="Search companies…"/>
179
+ <button id="selectAll" class="btn">Select all</button>
180
+ <button id="clearAll" class="btn">Clear</button>
181
+ </div>
182
+ <div id="chips" class="flex flex-wrap gap-2 mb-2"></div>
183
+ <div id="list" class="border border-[var(--border)] rounded-lg p-2 max-h-56 overflow-auto bg-[#0c1730]"></div>
184
+ </div>
185
+ </div>
186
+ <!-- Date range -->
187
+ <div>
188
+ <div class="hint mb-1">Date Range</div>
189
+ <div class="card">
190
+ <div class="flex justify-between text-xs text-[var(--muted)] mono mb-1">
191
+ <span id="rangeMinBadge">—</span><span id="rangeMaxBadge">—</span>
192
+ </div>
193
+ <div class="relative">
194
+ <div class="track"><div class="fill" id="rangeFill"></div></div>
195
+ <input type="range" min="0" max="100" value="0" id="rangeMin" class="range">
196
+ <input type="range" min="0" max="100" value="100" id="rangeMax" class="range">
197
+ </div>
198
+ <div class="mt-2 flex flex-wrap gap-2 text-sm">
199
+ <button class="btn quick" data-last="5">Last 5 Days</button>
200
+ <button class="btn quick" data-last="7">Last 7 Days</button>
201
+ <button class="btn quick" data-last="10">Last 10 Days</button>
202
+ <button class="btn quick" data-last="14">Last 14 Days</button>
203
+ <button class="btn quick" id="allData">All Data</button>
204
+ </div>
205
+ <div class="hint mt-4 mb-1">MAFE vs AFE</div>
206
+ <select id="mafeFilter" class="btn w-full">
207
+ <option value="ALL">All</option>
208
+ <option value="AFE">AFE</option>
209
+ <option value="MAFE">MAFE</option>
210
+ </select>
211
+
212
+ <div class="hint mt-4 mb-1">Module</div>
213
+ <select id="moduleFilter" class="btn w-full">
214
+ <option value="ALL">All</option>
215
+ </select>
216
+ </div>
217
+ </div>
218
+ </div>
219
+ </div>
220
+ </section>
221
+
222
+ <!-- BREAKDOWN -->
223
+ <section>
224
+ <div class="section"><h2>Breakdown</h2><div class="hint">Hours are DFL only; costs and KPIs are totals</div></div>
225
+ <div class="grid-fit">
226
+ <div class="card">
227
+ <h3 class="font-semibold mb-2">Hours</h3>
228
+ <div class="grid-fit">
229
+ <div class="badge"><span class="dot"></span><span class="label">ST Hours</span><span id="h_st" class="val mono ml-auto">—</span></div>
230
+ <div class="badge"><span class="dot" style="background:var(--blue)"></span><span class="label">OT Hours</span><span id="h_ot" class="val mono ml-auto">—</span></div>
231
+ <div class="badge"><span class="dot" style="background:var(--green)"></span><span class="label">DT Hours</span><span id="h_dt" class="val mono ml-auto">—</span></div>
232
+ <div class="badge"><span class="dot" style="background:var(--pink)"></span><span class="label">Total Hours</span><span id="h_total" class="val mono ml-auto">—</span></div>
233
+ </div>
234
+ </div>
235
+ <div class="card">
236
+ <h3 class="font-semibold mb-2">Cost</h3>
237
+ <div class="grid-fit">
238
+ <div class="badge"><span class="dot"></span><span class="label">ST TOT</span><span id="c_st" class="val mono ml-auto">—</span></div>
239
+ <div class="badge"><span class="dot" style="background:var(--blue)"></span><span class="label">OT TOT</span><span id="c_ot" class="val mono ml-auto">—</span></div>
240
+ <div class="badge"><span class="dot" style="background:var(--green)"></span><span class="label">DT TOT</span><span id="c_dt" class="val mono ml-auto">—</span></div>
241
+ <div class="badge"><span class="dot" style="background:#f6a3a3"></span><span class="label">TOTALS</span><span id="c_tot" class="val mono ml-auto">—</span></div>
242
+ </div>
243
+ </div>
244
+ </div>
245
+ </section>
246
+
247
+ <!-- TRENDS -->
248
+ <section>
249
+ <div class="section"><h2>Trends</h2><div class="hint">Daily & Cumulative; Top spenders; Pies below</div></div>
250
+
251
+ <!-- Full-width Daily -->
252
+ <div class="card mb-4">
253
+ <h3 class="font-semibold mb-2">Daily Cost (bars) + Cumulative (line)</h3>
254
+ <div class="chartbox" style="height:440px"><canvas id="chartDaily"></canvas></div>
255
+ </div>
256
+
257
+ <!-- Full-width Cumulative -->
258
+ <div class="card mb-6">
259
+ <h3 class="font-semibold mb-2">Cumulative Cost</h3>
260
+ <div class="chartbox" style="height:440px"><canvas id="chartCum"></canvas></div>
261
+ </div>
262
+
263
+ <!-- Contractor Compare -->
264
+ <div class="card mb-6">
265
+ <div class="flex items-center justify-between mb-2">
266
+ <h3 class="font-semibold">Contractor Compare</h3>
267
+ <span class="hint">Uses current filters and date window. If none selected, shows top 5.</span>
268
+ </div>
269
+ <div class="chartbox" style="height:420px"><canvas id="chartCompare"></canvas></div>
270
+ <div id="compareLegend" class="compare-legend-bottom"></div>
271
+ </div>
272
+
273
+ <!-- Top 10 -->
274
+ <div class="grid md:grid-cols-2 gap-4">
275
+ <div class="card">
276
+ <div class="flex items-center justify-between mb-2">
277
+ <h3 class="font-semibold">Top 10 Companies (Chart)</h3>
278
+ <span class="hint">Reflects current filters</span>
279
+ </div>
280
+ <div class="chartbox" style="height:360px"><canvas id="chartTop"></canvas></div>
281
+ </div>
282
+ <div class="card"><h3 class="font-semibold mb-2">Top 10 Companies (List)</h3><ol id="topList" class="space-y-2 text-sm"></ol></div>
283
+ </div>
284
+ <!-- MAFE vs AFE Detailed Tables -->
285
+ <section class="mt-8">
286
+ <div class="section"><h2>MAFE vs AFE — Last 10 Days</h2><div class="hint">Cost and hours breakdown</div></div>
287
+ <div class="grid md:grid-cols-2 gap-4">
288
+ <div class="card">
289
+ <h3 class="font-semibold mb-2">Cost Comparison</h3>
290
+ <div class="overflow-auto max-h-[500px]">
291
+ <table id="afeMafeCost" class="w-full border-collapse">
292
+ <thead>
293
+ <tr class="border-b border-[var(--border)]">
294
+ <th class="text-left p-2">Date</th>
295
+ <th class="text-right p-2">MAFE</th>
296
+ <th class="text-right p-2">Capital</th>
297
+ <th class="text-right p-2">Total</th>
298
+ </tr>
299
+ </thead>
300
+ <tbody class="divide-y divide-[var(--border)]"></tbody>
301
+ </table>
302
+ </div>
303
+ </div>
304
+ <div class="card">
305
+ <h3 class="font-semibold mb-2">Hours Comparison</h3>
306
+ <div class="overflow-auto max-h-[500px]">
307
+ <table id="afeMafeHours" class="w-full border-collapse">
308
+ <thead>
309
+ <tr class="border-b border-[var(--border)]">
310
+ <th class="text-left p-2">Date</th>
311
+ <th class="text-right p-2">MAFE</th>
312
+ <th class="text-right p-2">AFE</th>
313
+ <th class="text-right p-2">Total</th>
314
+ </tr>
315
+ </thead>
316
+ <tbody class="divide-y divide-[var(--border)]"></tbody>
317
+ </table>
318
+ </div>
319
+ </div>
320
+ </div>
321
+ </section>
322
+
323
+ <!-- Pies underneath with spacing -->
324
+ <div class="grid md:grid-cols-2 gap-4 mt-8">
325
+ <div class="card">
326
+ <h3 class="font-semibold mb-2">MAFE vs AFE</h3>
327
+ <div class="chartbox" style="height:320px"><canvas id="chartMafe"></canvas></div>
328
+ </div>
329
+ <div class="card">
330
+ <h3 class="font-semibold mb-2">Pie by WBS Element (Top 10)</h3>
331
+ <div class="chartbox" style="height:320px"><canvas id="chartWbs"></canvas></div>
332
+ </div>
333
+ </div>
334
+
335
+ <!-- Cost by Module with additional spacing -->
336
+ <section class="mt-8">
337
+ <div class="card">
338
+ <div class="flex items-center justify-between mb-2">
339
+ <h3 class="font-semibold">Cost by Module</h3>
340
+ <span class="hint">Uses current filters and date window.</span>
341
+ </div>
342
+ <div class="grid md:grid-cols-2 gap-4 items-start">
343
+ <div class="chartbox" style="height:260px"><canvas id="chartModule"></canvas></div>
344
+ <div>
345
+ <div class="flex items-center justify-between mb-2">
346
+ <div class="hint">Module breakdown</div>
347
+ <div class="flex gap-2">
348
+ <button id="moduleShowDollar" class="btn">Show $</button>
349
+ <button id="moduleShowPercent" class="btn">Show %</button>
350
+ </div>
351
+ </div>
352
+ <div class="overflow-auto max-h-[260px]">
353
+ <table id="moduleTable" class="w-full border-collapse">
354
+ <thead>
355
+ <tr class="border-b border-[var(--border)]">
356
+ <th class="text-left p-2">Module</th>
357
+ <th class="text-right p-2">Cost</th>
358
+ <th class="text-right p-2">% of Total</th>
359
+ <th class="text-right p-2">Rank</th>
360
+ </tr>
361
+ </thead>
362
+ <tbody class="divide-y divide-[var(--border)]"></tbody>
363
+ </table>
364
+ </div>
365
+ </div>
366
+ </div>
367
+ </div>
368
+ </section>
369
+ </section>
370
+
371
+ <!-- ===== Monte Carlo — Cost Projection ===== -->
372
+ <section id="monte-carlo-section" class="mt-6">
373
+ <div class="section"><h2>Monte Carlo — Cost Projection</h2><div class="hint">Fan bands P10 / P50 / P90</div></div>
374
+ <div class="card">
375
+ <div class="flex flex-wrap items-center gap-2">
376
+ <span class="hint uppercase text-xs mr-1">Rate Window</span>
377
+ <button class="btn" id="mc_win_30" data-window="30">30d</button>
378
+ <button class="btn" id="mc_win_45" data-window="45">45d</button>
379
+ <button class="btn" id="mc_win_60" data-window="60">60d</button>
380
+
381
+ <div class="mx-2"></div>
382
+
383
+ <label class="hint">Current Burn Rate ($/day)</label>
384
+ <input id="mc_manualRate" type="text" inputmode="decimal" class="btn mono" placeholder="auto" style="width:140px" />
385
+
386
+ <div class="mx-2"></div>
387
+
388
+ <label class="hint">±%</label>
389
+ <input id="mc_uncertainty" type="range" min="0" max="50" value="20" style="accent-color:#22e1e8;width:130px">
390
+ <span id="mc_uncertaintyLabel" class="mono hint" style="width:36px;text-align:center">20%</span>
391
+
392
+ <div class="mx-2"></div>
393
+
394
+ <label class="hint">Horizon</label>
395
+ <input id="mc_horizon" type="number" min="1" max="365" value="60" class="btn mono" style="width:80px">
396
+
397
+ <label class="hint ml-2">Iters</label>
398
+ <input id="mc_iters" type="number" min="100" max="20000" step="100" value="5000" class="btn mono" style="width:90px">
399
+
400
+ <div class="ml-auto flex gap-2">
401
+ <button id="mc_runBtn" class="btn">Run</button>
402
+ <button id="mc_exportBtn" class="btn">Export CSV</button>
403
+ </div>
404
+ </div>
405
+
406
+ <div class="hint mt-2">
407
+ 💡 Tip: Rate window averages recent daily burn. Or type a Current Burn Rate. Adjust ±%, horizon, and iters, then click <b>Run</b>. Export CSV to download results.
408
+ </div>
409
+
410
+ <div class="grid lg:grid-cols-12 gap-3 mt-3">
411
+ <div class="card lg:col-span-8" style="height:460px">
412
+ <canvas id="mc_fanChart" style="width:100%;height:100%"></canvas>
413
+ </div>
414
+
415
+ <div class="lg:col-span-4 grid gap-3">
416
+ <div class="card">
417
+ <div class="kpi-title hint">Current Total</div>
418
+ <div id="mc_kpiCurrent" class="value mono">$—</div>
419
+ </div>
420
+ <div class="card">
421
+ <div class="kpi-title hint">Projected Total (P10)</div>
422
+ <div id="mc_kpiP10" class="value mono" style="color:#22e1e8">$—</div>
423
+ </div>
424
+ <div class="card">
425
+ <div class="kpi-title hint">Projected Total (P50)</div>
426
+ <div id="mc_kpiP50" class="value mono" style="color:#ffc455">$—</div>
427
+ </div>
428
+ <div class="card">
429
+ <div class="kpi-title hint">Projected Total (P90)</div>
430
+ <div id="mc_kpiP90" class="value mono" style="color:#f08ac3">$—</div>
431
+ </div>
432
+ </div>
433
+ </div>
434
+ </div>
435
+ </section>
436
+ </main>
437
+
438
+ <script>
439
+ const $=(id)=>document.getElementById(id);
440
+ const fmt=(n)=> n?.toLocaleString(undefined,{style:'currency',currency:'USD',maximumFractionDigits:0}) ?? '—';
441
+ const fmt0=(n)=> isNaN(n)?'—':n.toLocaleString(undefined,{maximumFractionDigits:0});
442
+ const parseDate=(v)=>{
443
+ if(v==null||v==='')return null;
444
+ if(typeof v==='number'){const s=String(v);if(s.length===8)return new Date(+s.slice(0,4),+s.slice(4,6)-1,+s.slice(6,8));return new Date((v-25569)*86400*1000);}
445
+ if(typeof v==='string'){const s=v.trim();if(/^\\d{8}$/.test(s))return new Date(+s.slice(0,4),+s.slice(4,6)-1,+s.slice(6,8));const d=new Date(s);return isNaN(d)?null:d;}
446
+ const d=new Date(v);return isNaN(d)?null:d;
447
+ };
448
+ const parseNum=(v)=>{if(v==null||v==='')return NaN;if(typeof v==='number')return v; let s=v.toString().trim(); const neg=/^\\(.*\\)$/.test(s); s=s.replace(/^\\((.*)\\)$/,'$1').replace(/[$,]/g,''); const n=parseFloat(s); return isNaN(n)?NaN:(neg?-n:n);};
449
+ const norm=(s)=> (s??'').toString().trim().toLowerCase();
450
+ const isDFL = v => String(v||'').toUpperCase().includes('DFL');
451
+
452
+ let RAW=[], FILTERED=[], DAILY=[], CUM=[], TOP=[], charts={};
453
+ let ALL_DATES=[]; let COMPANIES=[], SELECTED=new Set(); let MAFE='ALL'; let MAFE_EXACT='ALL'; let MODULE='ALL';
454
+
455
+ // Define a harmonious color palette that fits the current theme
456
+ const chartColors = [
457
+ '#22e1e8', // teal from gradient
458
+ '#7ad7ff', // blue from gradient
459
+ '#a87fff', // purple accent
460
+ '#f5a524', // amber/yellow
461
+ '#86efac', // green accent
462
+ '#f08ac3', // pink accent
463
+ '#ffa055', // orange accent
464
+ '#35b5ff', // lighter blue
465
+ '#1dc6c2', // slightly deeper teal
466
+ '#c28bff' // lighter purple
467
+ ];
468
+
469
+ function mapRow(r){
470
+ const k=Object.fromEntries(Object.keys(r).map(x=>[norm(x),r[x]]));
471
+ return {
472
+ module:(k['module']||k['module name']||k['mod']||k['mdl']||'').toString().trim(),
473
+ ts:parseDate(k['ts date']||k['ts_date']||k['tsdate']||k['date']||k['ts']),
474
+ total:parseNum(k['grand total']||k['grandtotal']||k['total']||k['amount']||k['totals']),
475
+ company:(k['company']||k['vendor']||k['contractor']||'').toString().trim(),
476
+ staff:(k['staff']||k['labor type']||k['category']||k['staff type']||'').toString().trim(),
477
+ st_hours:parseNum(k['st hours']||k['sthours']||k['s thours']),
478
+ ot_hours:parseNum(k['ot hours']||k['othours']),
479
+ dt_hours:parseNum(k['dt hours']||k['dthours']),
480
+ st_tot:parseNum(k['st tot']||k['st total']||k['st cost']),
481
+ ot_tot:parseNum(k['ot tot']||k['ot total']||k['ot cost']),
482
+ dt_tot:parseNum(k['dt tot']||k['dt total']||k['dt cost']),
483
+ mafe_raw:(k['mafe vs afe']||k['mafe_vs_afe']||k['mafe/afe']||'').toString().trim(),
484
+ mafe:(()=>{
485
+ const raw=(k['mafe vs afe']||k['mafe_vs_afe']||k['mafe/afe']||'').toString().trim();
486
+ const u=raw.toUpperCase();
487
+ if(u.includes('MAFE')) return 'MAFE';
488
+ if(u.includes('AFE')) return 'AFE';
489
+ return u;
490
+ })(),
491
+ wbs:(k['wbs element']||k['wbs']||'').toString().trim()
492
+ };
493
+ }
494
+ const uniqueSorted = arr => Array.from(new Set(arr.filter(Boolean))).sort((a,b)=>a>b?1:-1);
495
+
496
+ // Company picker
497
+ function buildPicker(){
498
+ COMPANIES = uniqueSorted(RAW.map(r=>r.company));
499
+ const list=$('list'), chips=$('chips');
500
+ function renderList(){
501
+ list.innerHTML='';
502
+ const q=($('search').value||'').toLowerCase();
503
+ COMPANIES.filter(c=>!q||c.toLowerCase().includes(q)).forEach(c=>{
504
+ const row=document.createElement('label'); row.className='flex items-center gap-2 py-1';
505
+ row.innerHTML=`<input type="checkbox" ${SELECTED.has(c)?'checked':''} /><span>${c||'—'}</span>`;
506
+ const cb=row.querySelector('input');
507
+ cb.addEventListener('change',(e)=>{
508
+ if(e.target.checked) SELECTED.add(c); else SELECTED.delete(c);
509
+ renderChips();
510
+ applyFilters(); // Update filters immediately when selection changes
511
+ });
512
+ list.appendChild(row);
513
+ });
514
+ }
515
+ function renderChips(){
516
+ chips.innerHTML='';
517
+ if(!SELECTED.size){ chips.innerHTML='<span class="hint">No companies selected (all included)</span>'; return; }
518
+ SELECTED.forEach(c=>{
519
+ const s=document.createElement('span'); s.className='px-2 py-1 rounded-full border border-[var(--border)] bg-[#0b1830] text-sm'; s.textContent=c;
520
+ const x=document.createElement('span'); x.textContent=' ×'; x.style.cursor='pointer'; x.style.opacity='.7';
521
+ x.onclick=()=>{ SELECTED.delete(c); renderList(); renderChips(); applyFilters(); };
522
+ s.appendChild(x); chips.appendChild(s);
523
+ });
524
+ }
525
+ $('selectAll').onclick=()=>{
526
+ COMPANIES.forEach(c=>SELECTED.add(c));
527
+ renderList();
528
+ renderChips();
529
+ applyFilters();
530
+ };
531
+ $('clearAll').onclick=()=>{
532
+ SELECTED.clear();
533
+ renderList();
534
+ renderChips();
535
+ applyFilters();
536
+ };
537
+ $('search').oninput=renderList;
538
+ renderList(); renderChips();
539
+ }
540
+
541
+ // Range slider
542
+ function seedRange(){
543
+ ALL_DATES = uniqueSorted(RAW.map(r=>r.ts&&r.ts.toISOString().slice(0,10)));
544
+ const max=Math.max(ALL_DATES.length-1,0);
545
+ $('rangeMin').min=0; $('rangeMax').min=0;
546
+ $('rangeMin').max=max; $('rangeMax').max=max;
547
+ $('rangeMin').value=0; $('rangeMax').value=max;
548
+ updateRangeUI();
549
+ }
550
+ function updateRangeUI(){
551
+ const lo=Math.min(+$('rangeMin').value,+$('rangeMax').value), hi=Math.max(+$('rangeMin').value,+$('rangeMax').value);
552
+ $('rangeMin').value=lo; $('rangeMax').value=hi;
553
+ $('rangeMinBadge').textContent=ALL_DATES[lo]||'—';
554
+ $('rangeMaxBadge').textContent=ALL_DATES[hi]||'—';
555
+ const a=(ALL_DATES.length<=1)?0:lo/(ALL_DATES.length-1)*100;
556
+ const b=(ALL_DATES.length<=1)?100:hi/(ALL_DATES.length-1)*100;
557
+ const f=$('rangeFill'); f.style.left=a+'%'; f.style.right=(100-b)+'%';
558
+ applyFilters();
559
+ }
560
+ function setLast(n){ if(!ALL_DATES.length) return; $('rangeMax').value=ALL_DATES.length-1; $('rangeMin').value=Math.max(0,ALL_DATES.length-n); updateRangeUI(); }
561
+ function setAll(){ if(!ALL_DATES.length) return; $('rangeMin').value=0; $('rangeMax').value=ALL_DATES.length-1; updateRangeUI(); }
562
+
563
+ // Aggregation
564
+ function applyFilters(){
565
+ if(!RAW.length) return;
566
+ const lo=+$('rangeMin').value, hi=+$('rangeMax').value;
567
+ const loDate=ALL_DATES[Math.min(lo,hi)]||null, hiDate=ALL_DATES[Math.max(lo,hi)]||null;
568
+ const sel=Array.from(SELECTED.values());
569
+ FILTERED = RAW.filter(r=>{
570
+ const d=r.ts&&r.ts.toISOString().slice(0,10);
571
+ if(loDate && d<loDate) return false;
572
+ if(hiDate && d>hiDate) return false;
573
+ // Company filter - check if selected companies exist and match
574
+ if(sel.length > 0) {
575
+ const companyMatch = sel.some(selectedCompany => {
576
+ return (r.company || '').trim().toLowerCase() === selectedCompany.trim().toLowerCase();
577
+ });
578
+ if(!companyMatch) return false;
579
+ }
580
+ if(MAFE_EXACT!=='ALL' && (r.mafe_raw||'')!==MAFE_EXACT) return false;
581
+ if(MAFE!=='ALL' && (r.mafe||'').toUpperCase()!==MAFE) return false;
582
+ if(MODULE!=='ALL' && String(r.module||'').toUpperCase()!==String(MODULE).toUpperCase()) return false;
583
+ return true;
584
+ });
585
+ aggregateAndRender();
586
+ }
587
+ function aggregateAndRender(){
588
+ const by=new Map();
589
+ FILTERED.forEach(r=>{const d=r.ts&&r.ts.toISOString().slice(0,10); if(!d)return; by.set(d,(by.get(d)||0)+(r.total||0));});
590
+ DAILY=Array.from(by.entries()).sort((a,b)=>a[0].localeCompare(b[0])).map(([d,v])=>({d,v}));
591
+ let run=0; CUM=DAILY.map(x=>{run+=x.v; return {d:x.d,v:run};});
592
+
593
+ const comp = new Map();
594
+ FILTERED.forEach(r=>{ const c = r.company || '—'; comp.set(c, (comp.get(c)||0) + (r.total||0)); });
595
+ TOP = Array.from(comp.entries()).sort((a,b)=>b[1]-a[1]).slice(0,10);
596
+
597
+ const tot=CUM.length?CUM[CUM.length-1].v:0, avg=DAILY.length?tot/DAILY.length:0;
598
+ $('k_total').textContent=fmt(tot); $('k_avg').textContent=fmt(avg);
599
+ $('k_window').textContent=DAILY.length ? (DAILY[0].d+' → '+DAILY[DAILY.length-1].d) : '—';
600
+ const sum=(arr,k)=>arr.reduce((a,b)=>a+(+b[k]||0),0);
601
+ const DFILTERED = FILTERED.filter(x => isDFL(x.staff));
602
+ const hst=sum(DFILTERED,'st_hours'), hot=sum(DFILTERED,'ot_hours'), hdt=sum(DFILTERED,'dt_hours');
603
+ $('h_st').textContent=fmt0(hst); $('h_ot').textContent=fmt0(hot); $('h_dt').textContent=fmt0(hdt); $('h_total').textContent=fmt0(hst+hot+hdt);
604
+ const cst=sum(FILTERED,'st_tot'), cot=sum(FILTERED,'ot_tot'), cdt=sum(FILTERED,'dt_tot');
605
+ $('c_st').textContent=fmt0(cst); $('c_ot').textContent=fmt0(cot); $('c_dt').textContent=fmt0(cdt);
606
+ $('c_tot').textContent=fmt0(cst+cot+cdt);
607
+ drawCharts(); drawCompare(); drawTopList(TOP);
608
+ drawMafePie(); drawWbsPie(); drawModulePie();
609
+ updateMafeAfeTables(); // Add this to ensure tables update
610
+ calcForecast();
611
+ }
612
+ function updateMafeAfeTables() {
613
+ if (!FILTERED.length) return;
614
+
615
+ // Get last 10 unique dates from filtered data
616
+ const dates = Array.from(new Set(FILTERED.map(r => r.ts && r.ts.toISOString().slice(0,10))))
617
+ .filter(Boolean)
618
+ .sort((a,b) => new Date(a) - new Date(b))
619
+ .slice(-10);
620
+
621
+ // Group by date and MAFE/AFE
622
+ const costByDate = {};
623
+ const hoursByDate = {};
624
+
625
+ // Initialize all dates with zero values
626
+ dates.forEach(d => {
627
+ costByDate[d] = { MAFE: 0, AFE: 0 };
628
+ hoursByDate[d] = {
629
+ MAFE: { st: 0, ot: 0, dt: 0, total: 0 },
630
+ AFE: { st: 0, ot: 0, dt: 0, total: 0 }
631
+ };
632
+ });
633
+
634
+ // Process filtered data
635
+ FILTERED.forEach(r => {
636
+ const d = r.ts && r.ts.toISOString().slice(0,10);
637
+ if (!d || !dates.includes(d)) return;
638
+
639
+ const isMAFE = (r.mafe || '').toUpperCase().includes('MAFE');
640
+ const key = isMAFE ? 'MAFE' : 'AFE';
641
+
642
+ // Sum costs
643
+ costByDate[d][key] += r.total || 0;
644
+
645
+ // Sum hours (only DFL)
646
+ if (isDFL(r.staff)) {
647
+ hoursByDate[d][key].st += r.st_hours || 0;
648
+ hoursByDate[d][key].ot += r.ot_hours || 0;
649
+ hoursByDate[d][key].dt += r.dt_hours || 0;
650
+ hoursByDate[d][key].total += (r.st_hours || 0) + (r.ot_hours || 0) + (r.dt_hours || 0);
651
+ }
652
+ });
653
+
654
+ // Render cost table
655
+ const costTable = document.getElementById('afeMafeCost').querySelector('tbody');
656
+ costTable.innerHTML = dates.map(d => {
657
+ const mafe = costByDate[d].MAFE;
658
+ const afe = costByDate[d].AFE;
659
+ const total = mafe + afe;
660
+ return `
661
+ <tr class="hover:bg-[var(--panel)]">
662
+ <td class="p-2">${d}</td>
663
+ <td class="p-2 text-right mono">${fmt(mafe)}</td>
664
+ <td class="p-2 text-right mono">${fmt(afe)} </td>
665
+ <td class="p-2 text-right mono">${fmt(total)}</td>
666
+ </tr>
667
+ `;
668
+ }).join('');
669
+
670
+ // Render hours table
671
+ const hoursTable = document.getElementById('afeMafeHours').querySelector('tbody');
672
+ hoursTable.innerHTML = dates.map(d => {
673
+ const mafe = hoursByDate[d].MAFE.total;
674
+ const afe = hoursByDate[d].AFE.total;
675
+ return `
676
+ <tr class="hover:bg-[var(--panel)]">
677
+ <td class="p-2">${d}</td>
678
+ <td class="p-2 text-right mono">${fmt0(mafe)}</td>
679
+ <td class="p-2 text-right mono">${fmt0(afe)}</td>
680
+ <td class="p-2 text-right mono">${fmt0(mafe + afe)}</td>
681
+ </tr>
682
+ `;
683
+ }).join('');
684
+ }
685
+ function drawTopList(arr){
686
+ const ol = $('topList'); ol.innerHTML='';
687
+ if(!arr.length){ol.innerHTML='<li class="hint">No data</li>';return;}
688
+ const max=Math.max(...arr.map(t=>t[1]));
689
+ arr.forEach(([name,val],i)=>{
690
+ const li=document.createElement('li');
691
+ li.innerHTML = `<div class="flex items-center gap-2">
692
+ <span class="mono text-xs" style="width:1.5rem;opacity:.7">${(i+1).toString().padStart(2,'0')}</span>
693
+ <div class="flex-1 min-w-0">
694
+ <div class="flex justify-between gap-2">
695
+ <span class="truncate">${name||'—'}</span>
696
+ <span class="mono">${fmt(val)}</span>
697
+ </div>
698
+ <div style="height:6px;background:#0f1e39;border:1px solid var(--border);border-radius:999px;margin-top:4px;position:relative">
699
+ <div style="position:absolute;left:0;top:0;height:6px;border-radius:999px;background:linear-gradient(90deg,var(--teal),#7ad7ff);width:${max?(val/max*100).toFixed(1):0}%"></div>
700
+ </div>
701
+ </div>
702
+ </div>`;
703
+ ol.appendChild(li);
704
+ });
705
+ }
706
+ function drawCompare(){
707
+ const ctx = document.getElementById('chartCompare'); if(!ctx) return;
708
+ if (charts.compare){ charts.compare.destroy(); charts.compare=null; }
709
+ const legend = document.getElementById('compareLegend'); if(legend) legend.innerHTML='';
710
+ if (!FILTERED.length){ charts.compare = new Chart(ctx,{type:'line',data:{labels:[],datasets:[]},options:{responsive:true,maintainAspectRatio:false}}); return; }
711
+
712
+ const modeBtn = document.getElementById('compareMode');
713
+ const mode = modeBtn?.dataset.mode || 'absolute'; // 'absolute' or 'indexed'
714
+
715
+ // Build date index for current window
716
+ const dates = Array.from(new Set(FILTERED.map(r=>r.ts && r.ts.toISOString().slice(0,10)))).filter(Boolean).sort();
717
+ const dateIdx = new Map(dates.map((d,i)=>[d,i]));
718
+
719
+ // Choose companies: selected chips, or top 5 by filtered spend
720
+ let chosen = Array.from(SELECTED.values());
721
+ if(!chosen.length){
722
+ const comp = new Map();
723
+ FILTERED.forEach(r=>{ const c=r.company||'—'; comp.set(c,(comp.get(c)||0)+(r.total||0)); });
724
+ chosen = Array.from(comp.entries()).sort((a,b)=>b[1]-a[1]).slice(0,5).map(x=>x[0]);
725
+ }
726
+
727
+ // Build per-day and cumulative per company
728
+ const daily = {}; const series = {};
729
+ chosen.forEach(c=>{ daily[c]=new Array(dates.length).fill(0); series[c]=new Array(dates.length).fill(0); });
730
+ FILTERED.forEach(r=>{
731
+ const c=r.company||'—'; if(!daily.hasOwnProperty(c)) return;
732
+ const d=r.ts && r.ts.toISOString().slice(0,10); const i=dateIdx.get(d); if(i==null) return;
733
+ daily[c][i] += (r.total||0);
734
+ });
735
+ Object.keys(series).forEach(c=>{
736
+ let run=0; series[c] = daily[c].map(v=>{ run+=v; return run; }); // cumulative
737
+ });
738
+
739
+ // Optional indexing
740
+ if(mode==='indexed'){
741
+ Object.keys(series).forEach(c=>{
742
+ const arr = series[c];
743
+ const base = arr.find(v=>v>0) || 0;
744
+ if(base>0){ series[c] = arr.map(v=> (v/base)*100 ); }
745
+ });
746
+ }
747
+
748
+ // datasets and legend
749
+ const datasets = Object.keys(series).map((c,i)=>({
750
+ label:c,
751
+ data:series[c],
752
+ borderColor:chartColors[i%chartColors.length],
753
+ tension:.25,
754
+ pointRadius:0
755
+ }));
756
+
757
+ // Custom legend pills with latest values
758
+ if(legend){
759
+ Object.keys(series).forEach((c,i)=>{
760
+ const latest = series[c].length ? series[c][series[c].length-1] : 0;
761
+ const pill = document.createElement('div'); pill.className='compare-pill';
762
+ pill.innerHTML = `<span class="compare-dot" style="background:${chartColors[i%chartColors.length]}"></span><span>${c}</span><span class="val">${mode==='indexed'? latest.toFixed(0): (latest||0).toLocaleString()}</span>`;
763
+ pill.addEventListener('click',()=>{ // toggle dataset visibility
764
+ const ds = charts.compare.data.datasets[i]; ds.hidden = !ds.hidden; charts.compare.update();
765
+ pill.style.opacity = ds.hidden? .45 : 1;
766
+ });
767
+ legend.appendChild(pill);
768
+ });
769
+ }
770
+
771
+ charts.compare = new Chart(ctx,{
772
+ type:'line',
773
+ data:{ labels:dates, datasets },
774
+ options:{
775
+ responsive:true,
776
+ maintainAspectRatio:false,
777
+ plugins:{ legend:{display:false} },
778
+ interaction:{mode:'nearest',intersect:false},
779
+ scales:{
780
+ y:{
781
+ grid: { color: 'rgba(255,255,255,.05)' },
782
+ ticks: {
783
+ callback:(v)=> mode==='indexed'? (v|0) : (typeof v==='number'? v.toLocaleString():v)
784
+ }
785
+ },
786
+ x: {
787
+ grid: { color: 'rgba(255,255,255,.05)' }
788
+ }
789
+ }
790
+ }
791
+ });
792
+ }
793
+
794
+
795
+ function drawCharts(){
796
+ const labels=DAILY.map(x=>x.d);
797
+ ['daily','cum','top'].forEach(k=>{ if(charts[k]){charts[k].destroy(); charts[k]=null;} });
798
+ // Combined chart: Daily bars (left y) + Cumulative line (right y)
799
+ charts.daily = new Chart(document.getElementById('chartDaily'), {
800
+ data: {
801
+ labels,
802
+ datasets: [
803
+ { type:'bar', label:'Daily', data:DAILY.map(x=>x.v),
804
+ backgroundColor:'rgba(34,225,232,0.25)', borderColor:'#22e1e8', borderWidth:1, yAxisID:'y' },
805
+ { type:'line', label:'Cumulative', data:CUM.map(x=>x.v),
806
+ borderColor:'#7ad7ff', backgroundColor:'rgba(122,215,255,0.12)',
807
+ borderWidth:2, tension:0.25, pointRadius:0, yAxisID:'y1' }
808
+ ]
809
+ },
810
+ options:{
811
+ responsive:true, maintainAspectRatio:false,
812
+ interaction:{mode:'index',intersect:false},
813
+ plugins:{legend:{display:true}},
814
+ scales:{
815
+ x:{ grid:{color:'rgba(255,255,255,.05)'}, border:{color:'rgba(255,255,255,.1)'} },
816
+ y:{ position:'left', grid:{color:'rgba(255,255,255,.05)'}, border:{color:'rgba(255,255,255,.1)'},
817
+ ticks:{ callback:(v)=> v.toLocaleString() } },
818
+ y1:{ position:'right', grid:{ drawOnChartArea:false }, border:{color:'rgba(255,255,255,.1)'},
819
+ ticks:{ callback:(v)=> v.toLocaleString() } }
820
+ }
821
+ }
822
+ });
823
+
824
+ // Keep standalone cumulative chart below
825
+ const optsLine={
826
+ responsive:true, maintainAspectRatio:false, animation:false,
827
+ plugins:{legend:{display:false}},
828
+ scales:{
829
+ x:{ grid:{color:'rgba(255,255,255,.05)'}, border:{color:'rgba(255,255,255,.1)'} },
830
+ y:{ grid:{color:'rgba(255,255,255,.05)'}, border:{color:'rgba(255,255,255,.1)'},
831
+ ticks:{ callback:(v)=> v.toLocaleString() } }
832
+ }
833
+ };
834
+ charts.cum=new Chart(document.getElementById('chartCum'),{
835
+ type:'line',
836
+ data:{labels, datasets:[{label:'Cumulative', data:CUM.map(x=>x.v),
837
+ borderColor:'#7ad7ff', backgroundColor:'rgba(122,215,255,0.1)',
838
+ borderWidth:2, tension:0.25, pointRadius:0}]},
839
+ options:optsLine
840
+ });
841
+
842
+ charts.top=new Chart(document.getElementById('chartTop'),{
843
+ type:'bar',
844
+ data:{labels:TOP.map(t=>t[0]), datasets:[{data:TOP.map(t=>t[1]),
845
+ backgroundColor: TOP.map((_,i)=>['#22e1e8','#7ad7ff','#a87fff','#f5a524','#86efac','#f08ac3','#ffa055','#35b5ff','#1dc6c2','#c28bff'][i%10]),
846
+ borderColor:'#111', borderWidth:1}]},
847
+ options:optsLine
848
+ });
849
+ }
850
+
851
+ function pieOptions(){return {
852
+ responsive:true,
853
+ maintainAspectRatio:false,
854
+ plugins:{
855
+ legend:{
856
+ display:true,
857
+ position:'right',
858
+ labels:{
859
+ usePointStyle: true,
860
+ color: '#cfe0ff',
861
+ font: { size: 12 },
862
+ padding: 20
863
+ }
864
+ }
865
+ }
866
+ }}
867
+ function drawMafePie(){
868
+ const ctx=document.getElementById('chartMafe'); if(!ctx) return;
869
+ if (charts.mafe) charts.mafe.destroy();
870
+ if (!FILTERED.length){
871
+ charts.mafe = new Chart(ctx,{
872
+ type:'pie',
873
+ data:{labels:[],datasets:[{data:[]}]},
874
+ options:pieOptions()
875
+ });
876
+ return;
877
+ }
878
+ const totals=new Map();
879
+ FILTERED.forEach(r=>{const k=r.mafe_raw||'—'; totals.set(k,(totals.get(k)||0)+(r.total||0));});
880
+ const labels=Array.from(totals.keys());
881
+ const data=labels.map(k=>totals.get(k));
882
+
883
+ charts.mafe=new Chart(ctx,{
884
+ type:'pie',
885
+ data:{
886
+ labels,
887
+ datasets:[{
888
+ data,
889
+ backgroundColor: chartColors,
890
+ borderColor: '#0b1222',
891
+ borderWidth: 2
892
+ }]
893
+ },
894
+ options:pieOptions()
895
+ });
896
+ }
897
+ function drawWbsPie(){
898
+ const ctx=document.getElementById('chartWbs'); if(!ctx) return;
899
+ if (charts.wbs) charts.wbs.destroy();
900
+ if (!FILTERED.length){
901
+ charts.wbs = new Chart(ctx,{
902
+ type:'pie',
903
+ data:{labels:[],datasets:[{data:[]}]},
904
+ options:pieOptions()
905
+ });
906
+ return;
907
+ }
908
+ const totals=new Map();
909
+ FILTERED.forEach(r=>{const k=r.wbs||'—'; totals.set(k,(totals.get(k)||0)+(r.total||0));});
910
+ const top=Array.from(totals.entries()).sort((a,b)=>b[1]-a[1]).slice(0,10);
911
+ const labels=top.map(x=>x[0]); const data=top.map(x=>x[1]);
912
+
913
+ charts.wbs=new Chart(ctx,{
914
+ type:'pie',
915
+ data:{
916
+ labels,
917
+ datasets:[{
918
+ data,
919
+ backgroundColor: chartColors,
920
+ borderColor: '#0b1222',
921
+ borderWidth: 2
922
+ }]
923
+ },
924
+ options:pieOptions()
925
+ });
926
+ }
927
+
928
+
929
+ function drawModulePie(){
930
+ const ctx=document.getElementById('chartModule'); if(!ctx) return;
931
+ if (charts.module) charts.module.destroy();
932
+
933
+ const tbody = document.querySelector('#moduleTable tbody');
934
+ if (!FILTERED.length){
935
+ charts.module = new Chart(ctx,{ type:'doughnut', data:{labels:[],datasets:[{data:[]}]}, options:pieOptions() });
936
+ if(tbody) tbody.innerHTML = '<tr><td class="p-2 hint" colspan="4">No data</td></tr>';
937
+ return;
938
+ }
939
+
940
+ // Build totals per module
941
+ const totals=new Map();
942
+ FILTERED.forEach(r=>{const k=(r.module||'—'); totals.set(k,(totals.get(k)||0)+(r.total||0));});
943
+ // Sort by value desc
944
+ const entries = Array.from(totals.entries()).sort((a,b)=>b[1]-a[1]);
945
+ const labels=entries.map(x=>x[0]);
946
+ const data=entries.map(x=>x[1]);
947
+
948
+ charts.module=new Chart(ctx,{
949
+ type:'doughnut',
950
+ data:{
951
+ labels,
952
+ datasets:[{
953
+ data,
954
+ backgroundColor: chartColors,
955
+ borderColor: '#0b1222',
956
+ borderWidth: 2
957
+ }]
958
+ },
959
+ options:(function(){
960
+ const base=pieOptions();
961
+ base.cutout='58%';
962
+ if(!base.plugins) base.plugins={};
963
+ base.plugins.legend={display:false};
964
+ base.plugins.tooltip={callbacks:{label:(c)=>{
965
+ const v=c.parsed;
966
+ const sum = data.reduce((a,b)=>a+b,0);
967
+ const pct = sum? (v/sum*100).toFixed(1) : '—';
968
+ return `${c.label}: ${fmt(v)} (${pct}%)`;
969
+ }}};
970
+ base.onHover=(e,elements)=>{
971
+ const rows = document.querySelectorAll('#moduleTable tbody tr');
972
+ rows.forEach(r=>r.style.background='');
973
+ if(elements && elements.length){
974
+ const idx = elements[0].index;
975
+ const row = rows[idx];
976
+ if(row){ row.style.background='rgba(255,255,255,0.06)'; }
977
+ }
978
+ };
979
+ return base;
980
+ })()
981
+ });
982
+
983
+ // Render right-side table
984
+ if(tbody){
985
+ const totalSum = data.reduce((a,b)=>a+b,0);
986
+ tbody.innerHTML = labels.map((name, i)=>{
987
+ const val = data[i];
988
+ const pct = totalSum ? (val/totalSum*100) : 0;
989
+ return `
990
+ <tr data-index="${i}" class="hover:bg-[var(--panel)]">
991
+ <td class="p-2">${name||'—'}</td>
992
+ <td class="p-2 text-right mono">${fmt(val)}</td>
993
+ <td class="p-2 text-right mono">${pct.toFixed(1)}%</td>
994
+ <td class="p-2 text-right mono">${(i+1)}</td>
995
+ </tr>
996
+ `;
997
+ }).join('');
998
+
999
+ // Interactivity: hover and click syncing
1000
+ tbody.querySelectorAll('tr').forEach(tr=>{
1001
+ tr.addEventListener('mouseenter', (ev)=>{
1002
+ const idx = +ev.currentTarget.dataset.index;
1003
+ if(Number.isFinite(idx)){
1004
+ charts.module.setActiveElements([{datasetIndex:0, index:idx}]);
1005
+ charts.module.update();
1006
+ }
1007
+ });
1008
+ tr.addEventListener('mouseleave', ()=>{
1009
+ charts.module.setActiveElements([]);
1010
+ charts.module.update();
1011
+ });
1012
+ tr.addEventListener('click', (ev)=>{
1013
+ const idx = +ev.currentTarget.dataset.index;
1014
+ const ds = charts.module.data.datasets[0];
1015
+ if(ds._isoIndex === idx){
1016
+ delete ds._isoIndex;
1017
+ ds.backgroundColor = labels.map((_,i)=> chartColors[i%chartColors.length]);
1018
+ }else{
1019
+ ds._isoIndex = idx;
1020
+ ds.backgroundColor = labels.map((_,i)=> i===idx ? chartColors[i%chartColors.length] : 'rgba(255,255,255,0.15)');
1021
+ }
1022
+ charts.module.update();
1023
+ });
1024
+ });
1025
+
1026
+ // Toggle buttons for $ vs %
1027
+ const btn$ = document.getElementById('moduleShowDollar');
1028
+ const btnP = document.getElementById('moduleShowPercent');
1029
+ if(btn$ && btnP){
1030
+ btn$.onclick = ()=>{
1031
+ tbody.querySelectorAll('tr').forEach((tr,i)=>{
1032
+ const val = data[i];
1033
+ const pct = totalSum ? (val/totalSum*100) : 0;
1034
+ tr.children[1].textContent = fmt(val);
1035
+ tr.children[2].textContent = pct.toFixed(1) + '%';
1036
+ });
1037
+ };
1038
+ btnP.onclick = ()=>{
1039
+ tbody.querySelectorAll('tr').forEach((tr,i)=>{
1040
+ const val = data[i];
1041
+ const pct = totalSum ? (val/totalSum*100) : 0;
1042
+ tr.children[1].textContent = pct.toFixed(1) + '%';
1043
+ tr.children[2].textContent = fmt(val);
1044
+ });
1045
+ };
1046
+ }
1047
+ }
1048
+ }
1049
+ function
1050
+ calcForecast(){
1051
+ const t=$('targetDate').value?new Date($('targetDate').value):null;
1052
+ if(!t||!DAILY.length){$('k_forecast').textContent='—';$('k_forecast_formula').textContent='—';return;}
1053
+ const end=new Date(DAILY[DAILY.length-1].d);
1054
+ const rem=Math.max(Math.ceil((t-end)/86400000),0);
1055
+ const ac=CUM[CUM.length-1].v;
1056
+ const burn = DAILY.length ? ac/DAILY.length : 0; // average over selected window
1057
+ const eac=ac + rem*burn;
1058
+ $('k_forecast').textContent = fmt(eac);
1059
+ $('k_forecast_formula').textContent = `(${rem} days × ${fmt(burn)})`;
1060
+ }
1061
+
1062
+ // === Loader control ===
1063
+ (function(){
1064
+ const $ = (id)=>document.getElementById(id);
1065
+ const loader = $('cmLoader'), bar = $('cmBar'), pct = $('cmPct'), step = $('cmStep');
1066
+ // path animations setup
1067
+ const curve = document.getElementById('cmCurve');
1068
+ const dot = document.getElementById('cmDot');
1069
+ function animateCurve(){
1070
+ try{
1071
+ const len = curve.getTotalLength();
1072
+ curve.style.strokeDasharray = len + ' ' + len;
1073
+ curve.style.strokeDashoffset = len;
1074
+ curve.style.animation = 'cmDraw 1.6s ease forwards, cmDash 1.2s ease-in-out infinite alternate';
1075
+ // dot motion along path
1076
+ let t=0; const dur=1800;
1077
+ function tick(ts0){
1078
+ const ts = performance.now();
1079
+ const p = (ts % dur) / dur;
1080
+ const pt = curve.getPointAtLength(p*len);
1081
+ dot.setAttribute('cx', pt.x); dot.setAttribute('cy', pt.y);
1082
+ dot.style.animation = 'cmGlow 1.6s ease-in-out infinite';
1083
+ requestAnimationFrame(tick);
1084
+ } requestAnimationFrame(tick);
1085
+ }catch(_){}
1086
+ }
1087
+ window.CM_LOADER = {
1088
+ show(msg='Loading…', v=0){
1089
+ if(loader.style.display!=='flex'){
1090
+ loader.style.display='flex';
1091
+ animateCurve();
1092
+ }
1093
+ step.textContent=msg;
1094
+ bar.style.width = Math.max(0,Math.min(100,v)) + '%';
1095
+ pct.textContent = Math.round(Math.max(v,0)) + '%';
1096
+ },
1097
+ step(msg, v){
1098
+ step.textContent=msg;
1099
+ if(typeof v==='number'){
1100
+ bar.style.width = Math.max(0,Math.min(100,v)) + '%';
1101
+ pct.textContent = Math.round(Math.max(v,0)) + '%';
1102
+ }
1103
+ },
1104
+ hide(){
1105
+ loader.style.display='none';
1106
+ bar.style.width='0%';
1107
+ pct.textContent='0%';
1108
+ step.textContent='';
1109
+ }
1110
+ };
1111
+ })();
1112
+
1113
+ // --- toast control ---
1114
+ (function(){
1115
+ const t = document.getElementById('cmToast');
1116
+ const msg = document.getElementById('cmToastMsg');
1117
+ window.CM_TOAST = {
1118
+ show(m='Loading file… please wait'){
1119
+ if(t){
1120
+ msg.textContent=m;
1121
+ t.style.display='block';
1122
+ }
1123
+ },
1124
+ hide(){
1125
+ if(t){
1126
+ t.style.display='none';
1127
+ }
1128
+ }
1129
+ };
1130
+ })();
1131
+
1132
+ // Upload & controls
1133
+ document.getElementById('fileLabel').addEventListener('keydown',(e)=>{
1134
+ if(e.key==='Enter' || e.key===' '){
1135
+ e.preventDefault();
1136
+ const inp=$('file');
1137
+ if(inp){
1138
+ try{
1139
+ inp.click();
1140
+ }catch(_){}
1141
+ }
1142
+ }
1143
+ });
1144
+ document.getElementById('fileLabel').addEventListener('pointerdown',()=>{ $('file').value=''; });
1145
+ // Extra safety: explicit click to open the chooser across browsers
1146
+ document.getElementById('fileLabel').addEventListener('click',(e)=>{
1147
+ e.preventDefault();
1148
+ const inp=$('file');
1149
+ if(inp){
1150
+ try{
1151
+ inp.click();
1152
+ }catch(_){}
1153
+ }
1154
+ });
1155
+ $('file').addEventListener('change',(e)=>{
1156
+ const f=e.target.files && e.target.files[0];
1157
+ if(!f) return;
1158
+ const spinner = document.getElementById('uploadSpinner');
1159
+ if(spinner) {
1160
+ spinner.style.display = 'block';
1161
+ document.querySelector('.cm-dot').style.display = 'none';
1162
+ }
1163
+ CM_TOAST.show('Uploading file...');
1164
+ $('filename').textContent=f.name;
1165
+ const lower=f.name.toLowerCase();
1166
+ const afterLoad=()=>{
1167
+ FILTERED=RAW.slice();
1168
+ seedRange();
1169
+ buildPicker();
1170
+ buildMafeDropdown();
1171
+ buildModuleDropdown();
1172
+ applyFilters();
1173
+ };
1174
+ if(lower.endsWith('.csv')){
1175
+ try{ CM_LOADER.step('Parsing CSV…', 18); }catch(_){}
1176
+ Papa.parse(f,{header:true,skipEmptyLines:true,dynamicTyping:true,complete:r=>{
1177
+ try{ CM_LOADER.step('Mapping rows…', 55); }catch(_){}
1178
+ RAW=r.data.map(mapRow).filter(x=>x.ts&&!isNaN(x.total));
1179
+ try{ CM_LOADER.step('Aggregating…', 72); }catch(_){}
1180
+ if(!RAW.length){alert('No valid rows'); CM_TOAST.hide(); return;}
1181
+ afterLoad();
1182
+ try{ CM_LOADER.step('Rendering…', 90); }catch(_){}
1183
+ CM_TOAST.hide();
1184
+ const spinner = document.getElementById('uploadSpinner');
1185
+ if(spinner) {
1186
+ spinner.style.display = 'none';
1187
+ document.querySelector('.cm-dot').style.display = 'block';
1188
+ }
1189
+ }});
1190
+ }else{
1191
+ const fr=new FileReader();
1192
+ fr.onload=ev=>{
1193
+ try{ CM_LOADER.step('Reading workbook…', 28); }catch(_){}
1194
+ const wb=XLSX.read(new Uint8Array(ev.target.result),{type:'array'});
1195
+ const sh=wb.SheetNames[0];
1196
+ const js=XLSX.utils.sheet_to_json(wb.Sheets[sh],{defval:'',raw:true});
1197
+ try{ CM_LOADER.step('Mapping rows…', 56); }catch(_){}
1198
+ RAW=js.map(mapRow).filter(x=>x.ts&&!isNaN(x.total));
1199
+ if(!RAW.length){alert('No valid rows');return;}
1200
+ afterLoad();
1201
+ try{ CM_LOADER.step('Rendering…', 90); }catch(_){}
1202
+ };
1203
+ fr.readAsArrayBuffer(f);
1204
+ }
1205
+ });
1206
+ document.addEventListener('click',e=>{
1207
+ if(e.target.matches('.quick[data-last]')){ e.preventDefault(); setLast(+e.target.dataset.last); }
1208
+ if(e.target.id==='allData'){ e.preventDefault(); setAll(); }
1209
+ });
1210
+ $('rangeMin').addEventListener('input',updateRangeUI);
1211
+ $('rangeMax').addEventListener('input',updateRangeUI);
1212
+ $('targetDate').addEventListener('change', calcForecast);
1213
+ $('exportCsv').onclick=()=>{
1214
+ if(!DAILY.length) return;
1215
+ const csv="Date,Actual,Cumulative\n"+DAILY.map((d,i)=>`${d.d},${d.v},${CUM[i].v}`).join('\n');
1216
+ saveAs(new Blob([csv],{type:'text/csv'}),"CostMaster_Filtered.csv");
1217
+ };
1218
+ $('resetAll').onclick=()=>{
1219
+ RAW=[];FILTERED=[];DAILY=[];CUM=[];TOP=[];ALL_DATES=[];SELECTED.clear(); MAFE='ALL'; MODULE='ALL';
1220
+ Object.values(charts).forEach(c=>c?.destroy()); charts={};
1221
+ ['k_total','k_window','k_avg','k_forecast','k_forecast_formula','h_st','h_ot','h_dt','c_st','c_ot','c_dt','c_tot'].forEach(id=>$(id).textContent='—');
1222
+ $('list').innerHTML=''; $('chips').innerHTML=''; $('filename').textContent='No file selected';
1223
+ $('topList').innerHTML=''; $('rangeMinBadge').textContent='—'; $('rangeMaxBadge').textContent='—';
1224
+ $('file').value=''; $('mafeFilter').value='ALL'; MAFE_EXACT='ALL'; $('moduleFilter').value='ALL';
1225
+ $('search').value=''; $('targetDate').value='';
1226
+ $('rangeMin').value=0; $('rangeMax').value=0;
1227
+ };
1228
+ $('mafeFilter').addEventListener('change', (e)=>{ const v=e.target.value; MAFE_EXACT=v; MAFE='ALL'; applyFilters(); });
1229
+
1230
+ function buildMafeDropdown(){
1231
+ const sel = document.getElementById('mafeFilter');
1232
+ if(!sel) return;
1233
+ const uniques = Array.from(new Set(RAW.map(r => (r.mafe_raw||'').trim()).filter(Boolean))).sort((a,b)=>a.localeCompare(b));
1234
+ const cur = sel.value;
1235
+ sel.innerHTML = '';
1236
+ const optAll = document.createElement('option'); optAll.value='ALL'; optAll.textContent='All'; sel.appendChild(optAll);
1237
+ uniques.forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; sel.appendChild(o); });
1238
+ sel.value = uniques.includes(cur) ? cur : 'ALL';
1239
+ }
1240
+
1241
+ function buildModuleDropdown(){
1242
+ const sel = document.getElementById('moduleFilter');
1243
+ if(!sel) return;
1244
+ const uniques = Array.from(new Set(RAW.map(r => (r.module||'').trim()).filter(Boolean))).sort((a,b)=>a.localeCompare(b));
1245
+ const cur = sel.value;
1246
+ sel.innerHTML = '';
1247
+ const optAll = document.createElement('option'); optAll.value='ALL'; optAll.textContent='All'; sel.appendChild(optAll);
1248
+ uniques.forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; sel.appendChild(o); });
1249
+ sel.value = uniques.includes(cur) ? cur : 'ALL';
1250
+ sel.onchange = (e)=>{ MODULE = e.target.value || 'ALL'; applyFilters(); };
1251
+ }
1252
+
1253
+ // Set up interactive features
1254
+ document.addEventListener('DOMContentLoaded', () => {
1255
+ // Initialize any elements that need setup after the DOM is loaded
1256
+ // The file input handling is already set up in the script above
1257
+ });
1258
+ </script>
1259
+
1260
+ <script>
1261
+ // ===== Safe-Mode PDF Export (no globals, white pages, aspect-fit) =====
1262
+ (function(){
1263
+ function addImageFit(pdf, dataURL, x, y, maxW, maxH){
1264
+ const img = new Image();
1265
+ return new Promise(res=>{
1266
+ img.onload = () => {
1267
+ const iw = img.width, ih = img.height;
1268
+ const r = Math.min(maxW/iw, maxH/ih);
1269
+ const w = Math.max(10, Math.floor(iw*r));
1270
+ const h = Math.max(10, Math.floor(ih*r));
1271
+ pdf.addImage(dataURL, 'PNG', x, y, w, h, undefined, 'FAST');
1272
+ res();
1273
+ };
1274
+ img.src = dataURL;
1275
+ });
1276
+ }
1277
+ async function snap(selector, scale=2){
1278
+ const el = document.querySelector(selector);
1279
+ if(!el) return null;
1280
+ const canvas = await html2canvas(el, {backgroundColor:'#ffffff', scale});
1281
+ return canvas.toDataURL('image/png', 0.98);
1282
+ }
1283
+ async function exportReportPDF(){
1284
+ try{
1285
+ if(!(window.jspdf && window.jspdf.jsPDF) || !window.html2canvas){
1286
+ alert('PDF libraries not loaded. Check the two <script> tags in <head>.');
1287
+ return;
1288
+ }
1289
+ const { jsPDF } = window.jspdf;
1290
+ const pdf = new jsPDF({orientation:'landscape', unit:'pt', format:'a4'});
1291
+ const M = 36, W = pdf.internal.pageSize.getWidth(), H = pdf.internal.pageSize.getHeight();
1292
+ const BOXW = W - 2*M, BOXH = H - 2*M;
1293
+
1294
+ // Title Page
1295
+ pdf.setFillColor(255,255,255); pdf.rect(0,0,W,H,'F'); pdf.setTextColor(0,0,0);
1296
+ pdf.setFont('helvetica','bold'); pdf.setFontSize(20); pdf.text('Cost Master — Project Report', M, 46);
1297
+ pdf.setFont('helvetica','normal'); pdf.setFontSize(12);
1298
+ const now = new Date();
1299
+ const dr = (document.getElementById('dateRange')?.textContent || document.getElementById('k_window')?.textContent || '').trim();
1300
+ const mafeRows = FILTERED.filter(r => (r.mafe||'').toUpperCase().includes('MAFE')).length;
1301
+ const afeRows = FILTERED.filter(r => (r.mafe||'').toUpperCase().includes('AFE')).length;
1302
+ const tot = (document.querySelector('#ov_cost b')?.textContent || document.getElementById('k_total')?.textContent || '').trim();
1303
+ const burn= (document.querySelector('#ov_burn b')?.textContent || document.getElementById('k_avg')?.textContent || '').trim();
1304
+ const hST = (document.getElementById('h_st')?.textContent || '').trim();
1305
+ const hOT = (document.getElementById('h_ot')?.textContent || '').trim();
1306
+ const hDT = (document.getElementById('h_dt')?.textContent || '').trim();
1307
+ const hTOT= (document.getElementById('h_tot')?.textContent || document.getElementById('h_total')?.textContent || '').trim();
1308
+ let y = 76;
1309
+ pdf.text(`Generated: ${now.toLocaleString()}`, M, y); y+=20;
1310
+ if(dr){ pdf.text(`Date Range: ${dr}`, M, y); y+=26; }
1311
+ if(tot){ pdf.text(`Total Cost: ${tot}`, M, y); y+=18; }
1312
+ if(burn){ pdf.text(`Avg Daily Burn: ${burn}`, M, y); y+=18; }
1313
+ pdf.text(`Hours (DFL): ST ${hST} | OT ${hOT} | DT ${hDT} | Total ${hTOT}`, M, y);
1314
+
1315
+ // Helper to add chart by selector
1316
+ async function addChart(title, selector){
1317
+ const dataURL = await snap(selector);
1318
+ if(!dataURL) return;
1319
+ pdf.addPage('a4','landscape');
1320
+ pdf.setFillColor(255,255,255); pdf.rect(0,0,W,H,'F'); pdf.setTextColor(0,0,0);
1321
+ pdf.setFont('helvetica','bold'); pdf.setFontSize(16); pdf.text(title, M, 40);
1322
+ await addImageFit(pdf, dataURL, M, 56, BOXW, BOXH-40);
1323
+ }
1324
+ await addChart('Daily Cost (bars) + Cumulative (line)', '#chartDaily');
1325
+ await addChart('Cumulative Cost', '#chartCum');
1326
+ await addChart('Cost by Module', '#chartModule');
1327
+ await addChart('Top Categories', '#chartTop');
1328
+ await addChart('Contractor Compare', '#chartCompare');
1329
+ await addChart('MAFE vs AFE - Cost Comparison', '#afeMafeCost');
1330
+ await addChart('MAFE vs AFE - Hours Comparison', '#afeMafeHours');
1331
+ // Page numbers
1332
+ const pc = pdf.getNumberOfPages();
1333
+ pdf.setFont('helvetica','normal'); pdf.setFontSize(9);
1334
+ for(let i=1;i<=pc;i++){ pdf.setPage(i); pdf.text(String(i)+' / '+String(pc), W - M, H - 10, {align:'right'}); }
1335
+
1336
+ pdf.save('CostMaster_Report.pdf');
1337
+ }catch(err){ console.error(err); alert('PDF export failed: '+(err?.message||err)); }
1338
+ }
1339
+ document.addEventListener('DOMContentLoaded', ()=>{
1340
+ const btn = document.getElementById('exportPdf');
1341
+ if(btn){ btn.onclick = exportReportPDF; }
1342
+ });
1343
+ })();
1344
+ </script>
1345
+
1346
+
1347
+ <script>
1348
+ (function(){
1349
+ const $ = id => document.getElementById(id);
1350
+ const pick = (...sels)=>sels.map(s=>document.querySelector(s)).find(Boolean);
1351
+ const toNum = v => Number(String(v||'').replace(/[^\d.-]/g,'')) || 0;
1352
+ const usd = n => n.toLocaleString(undefined,{style:'currency',currency:'USD',maximumFractionDigits:0});
1353
+ const parseNum = v => { if(v==null) return 0; const s=(''+v).replace(/[^\d.\-]/g,''); const n=Number(s); return Number.isFinite(n)?n:0; };
1354
+
1355
+ const manualRateEl = $('mc_manualRate');
1356
+ const uncEl = $('mc_uncertainty'), uncLbl=$('mc_uncertaintyLabel');
1357
+ const HEl = $('mc_horizon'), NEl = $('mc_iters');
1358
+ const runBtn = $('mc_runBtn'), exportBtn = $('mc_exportBtn');
1359
+ const kCur=$('mc_kpiCurrent'), k10=$('mc_kpiP10'), k50=$('mc_kpiP50'), k90=$('mc_kpiP90');
1360
+ const winBtns = ['mc_win_30','mc_win_45','mc_win_60'].map(id=>$(id));
1361
+ let activeWindow = 30;
1362
+ winBtns.forEach(b=>b&&b.addEventListener('click',()=>{
1363
+ activeWindow = +b.dataset.window;
1364
+ winBtns.forEach(x=>x.style.opacity = (x===b)? '1' : '.7');
1365
+ }));
1366
+ if(winBtns[0]) winBtns[0].style.opacity='1';
1367
+
1368
+ uncEl.addEventListener('input', ()=> uncLbl.textContent = (uncEl.value|0)+'%');
1369
+
1370
+ function boxMuller(){
1371
+ let u=0,v=0; while(u===0) u=Math.random(); while(v===0) v=Math.random();
1372
+ return Math.sqrt(-2.0*Math.log(u))*Math.cos(2.0*Math.PI*v);
1373
+ }
1374
+ function buildDateLabels(H){
1375
+ const out=[]; const base=new Date();
1376
+ for(let i=1;i<=H;i++){ const d=new Date(base); d.setDate(d.getDate()+i); out.push(d.toLocaleDateString(undefined,{month:'short',day:'2-digit'})); }
1377
+ return out;
1378
+ }
1379
+ function getCurrentTotal(){ return toNum(pick('#k_total','#total-cost')?.textContent); }
1380
+ function getAvgBurn(){ return toNum(pick('#k_avg','#avg-daily-burn')?.textContent); }
1381
+
1382
+ const ctx = document.getElementById('mc_fanChart').getContext('2d');
1383
+ let chart = new Chart(ctx, {
1384
+ type:'line',
1385
+ data:{datasets:[]},
1386
+ options:{
1387
+ responsive:true, maintainAspectRatio:false, animation:false,
1388
+ interaction:{mode:'index', intersect:false},
1389
+ plugins:{legend:{display:true, labels:{color:'#cfe0ff'}}},
1390
+ scales:{
1391
+ x:{ticks:{color:'#cfe0ff'}, grid:{color:'rgba(255,255,255,.06)'}},
1392
+ y:{ticks:{color:'#cfe0ff'}, grid:{color:'rgba(255,255,255,.06)'}}
1393
+ }
1394
+ }
1395
+ });
1396
+
1397
+ function exportCSV(labels, base, p10, p50, p90){
1398
+ const header='Date,Baseline,P10,P50,P90\n';
1399
+ const rows=labels.map((lab,i)=>`${lab},${Math.round(base)},${Math.round(p10[i]||0)},${Math.round(p50[i]||0)},${Math.round(p90[i]||0)}`).join('\n');
1400
+ const blob=new Blob([header+rows],{type:'text/csv'}); const url=URL.createObjectURL(blob);
1401
+ const a=document.createElement('a'); a.href=url; a.download='mc_cost_projection.csv'; a.click(); URL.revokeObjectURL(url);
1402
+ }
1403
+
1404
+ function runMC(){
1405
+ const baseRate = parseNum(manualRateEl.value) || getAvgBurn();
1406
+ const U = (+uncEl.value||0)/100;
1407
+ const H = Math.max(1, +HEl.value||60);
1408
+ const N = Math.max(100, +NEl.value||5000);
1409
+ const currentTotal = getCurrentTotal();
1410
+
1411
+ if(kCur) kCur.textContent = usd(currentTotal||0);
1412
+
1413
+ if(!(baseRate>0)){
1414
+ chart.data.datasets=[]; chart.update();
1415
+ [k10,k50,k90].forEach(k=>k.textContent='$—');
1416
+ return;
1417
+ }
1418
+
1419
+ const series=[];
1420
+ for(let i=0;i<N;i++){
1421
+ let cum=currentTotal; const path=new Array(H);
1422
+ for(let d=0;d<H;d++){
1423
+ const z=boxMuller();
1424
+ const today=Math.max(0, baseRate + z*(U*baseRate));
1425
+ cum+=today; path[d]=cum;
1426
+ }
1427
+ series.push(path);
1428
+ }
1429
+
1430
+ const p10=[],p50=[],p90=[];
1431
+ for(let d=0;d<H;d++){
1432
+ const col=series.map(r=>r[d]).sort((a,b)=>a-b);
1433
+ p10.push(col[Math.floor(0.10*(N-1))]);
1434
+ p50.push(col[Math.floor(0.50*(N-1))]);
1435
+ p90.push(col[Math.floor(0.90*(N-1))]);
1436
+ }
1437
+
1438
+ if(k10) k10.textContent=usd(p10[H-1]);
1439
+ if(k50) k50.textContent=usd(p50[H-1]);
1440
+ if(k90) k90.textContent=usd(p90[H-1]);
1441
+
1442
+ const labels=buildDateLabels(H);
1443
+ chart.data.datasets=[
1444
+ {label:'P90',data:p90,borderColor:'#f08ac3',backgroundColor:'rgba(240,138,195,.15)',fill:1,tension:.25},
1445
+ {label:'P50',data:p50,borderColor:'#ffc455',backgroundColor:'rgba(255,196,85,.15)',fill:false,tension:.25},
1446
+ {label:'P10',data:p10,borderColor:'#22e1e8',backgroundColor:'rgba(34,225,232,.15)',fill:-1,tension:.25}
1447
+ ];
1448
+ chart.data.labels=labels; chart.update();
1449
+
1450
+ exportBtn.onclick=()=>exportCSV(labels,currentTotal,p10,p50,p90);
1451
+ }
1452
+
1453
+ // initial paint
1454
+ const ct0 = getCurrentTotal();
1455
+ if (kCur && ct0>0) kCur.textContent = usd(ct0);
1456
+ if (!manualRateEl.value) {
1457
+ const b0 = getAvgBurn(); if (b0>0) manualRateEl.value = Math.round(b0);
1458
+ }
1459
+ runBtn.addEventListener('click', runMC);
1460
+
1461
+ // keep synced if overview changes
1462
+ let raf=0; const sync=()=>{ if(raf) cancelAnimationFrame(raf); raf=requestAnimationFrame(()=>{
1463
+ const ct=getCurrentTotal(); if(kCur && ct>0) kCur.textContent=usd(ct);
1464
+ if(!manualRateEl.value){ const b=getAvgBurn(); if(b>0) manualRateEl.value=Math.round(b); }
1465
+ });};
1466
+ ['#k_total','#total-cost','#k_avg','#avg-daily-burn'].forEach(sel=>{
1467
+ const n=document.querySelector(sel); if(n) new MutationObserver(sync).observe(n,{subtree:true,childList:true,characterData:true});
1468
+ });
1469
+ })();
1470
+ </script>
1471
+ </body>
1472
+ </html>