Kaliman-1981 commited on
Commit
5972fea
·
verified ·
1 Parent(s): 25c7122

Add 3 files

Browse files
Files changed (3) hide show
  1. README.md +7 -5
  2. index.html +1470 -19
  3. prompts.txt +2 -0
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Costmaster2 0
3
- emoji: 🦀
4
- colorFrom: red
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: costmaster2-0
3
+ emoji: ⚛️
4
+ colorFrom: gray
5
+ colorTo: gray
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - QwenSite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,1470 @@
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';
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';
1225
+ };
1226
+ $('mafeFilter').addEventListener('change', (e)=>{ const v=e.target.value; MAFE_EXACT=v; MAFE='ALL'; applyFilters(); });
1227
+
1228
+ function buildMafeDropdown(){
1229
+ const sel = document.getElementById('mafeFilter');
1230
+ if(!sel) return;
1231
+ const uniques = Array.from(new Set(RAW.map(r => (r.mafe_raw||'').trim()).filter(Boolean))).sort((a,b)=>a.localeCompare(b));
1232
+ const cur = sel.value;
1233
+ sel.innerHTML = '';
1234
+ const optAll = document.createElement('option'); optAll.value='ALL'; optAll.textContent='All'; sel.appendChild(optAll);
1235
+ uniques.forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; sel.appendChild(o); });
1236
+ sel.value = uniques.includes(cur) ? cur : 'ALL';
1237
+ }
1238
+
1239
+ function buildModuleDropdown(){
1240
+ const sel = document.getElementById('moduleFilter');
1241
+ if(!sel) return;
1242
+ const uniques = Array.from(new Set(RAW.map(r => (r.module||'').trim()).filter(Boolean))).sort((a,b)=>a.localeCompare(b));
1243
+ const cur = sel.value;
1244
+ sel.innerHTML = '';
1245
+ const optAll = document.createElement('option'); optAll.value='ALL'; optAll.textContent='All'; sel.appendChild(optAll);
1246
+ uniques.forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; sel.appendChild(o); });
1247
+ sel.value = uniques.includes(cur) ? cur : 'ALL';
1248
+ sel.onchange = (e)=>{ MODULE = e.target.value || 'ALL'; applyFilters(); };
1249
+ }
1250
+
1251
+ // Set up interactive features
1252
+ document.addEventListener('DOMContentLoaded', () => {
1253
+ // Initialize any elements that need setup after the DOM is loaded
1254
+ // The file input handling is already set up in the script above
1255
+ });
1256
+ </script>
1257
+
1258
+ <script>
1259
+ // ===== Safe-Mode PDF Export (no globals, white pages, aspect-fit) =====
1260
+ (function(){
1261
+ function addImageFit(pdf, dataURL, x, y, maxW, maxH){
1262
+ const img = new Image();
1263
+ return new Promise(res=>{
1264
+ img.onload = () => {
1265
+ const iw = img.width, ih = img.height;
1266
+ const r = Math.min(maxW/iw, maxH/ih);
1267
+ const w = Math.max(10, Math.floor(iw*r));
1268
+ const h = Math.max(10, Math.floor(ih*r));
1269
+ pdf.addImage(dataURL, 'PNG', x, y, w, h, undefined, 'FAST');
1270
+ res();
1271
+ };
1272
+ img.src = dataURL;
1273
+ });
1274
+ }
1275
+ async function snap(selector, scale=2){
1276
+ const el = document.querySelector(selector);
1277
+ if(!el) return null;
1278
+ const canvas = await html2canvas(el, {backgroundColor:'#ffffff', scale});
1279
+ return canvas.toDataURL('image/png', 0.98);
1280
+ }
1281
+ async function exportReportPDF(){
1282
+ try{
1283
+ if(!(window.jspdf && window.jspdf.jsPDF) || !window.html2canvas){
1284
+ alert('PDF libraries not loaded. Check the two <script> tags in <head>.');
1285
+ return;
1286
+ }
1287
+ const { jsPDF } = window.jspdf;
1288
+ const pdf = new jsPDF({orientation:'landscape', unit:'pt', format:'a4'});
1289
+ const M = 36, W = pdf.internal.pageSize.getWidth(), H = pdf.internal.pageSize.getHeight();
1290
+ const BOXW = W - 2*M, BOXH = H - 2*M;
1291
+
1292
+ // Title Page
1293
+ pdf.setFillColor(255,255,255); pdf.rect(0,0,W,H,'F'); pdf.setTextColor(0,0,0);
1294
+ pdf.setFont('helvetica','bold'); pdf.setFontSize(20); pdf.text('Cost Master — Project Report', M, 46);
1295
+ pdf.setFont('helvetica','normal'); pdf.setFontSize(12);
1296
+ const now = new Date();
1297
+ const dr = (document.getElementById('dateRange')?.textContent || document.getElementById('k_window')?.textContent || '').trim();
1298
+ const mafeRows = FILTERED.filter(r => (r.mafe||'').toUpperCase().includes('MAFE')).length;
1299
+ const afeRows = FILTERED.filter(r => (r.mafe||'').toUpperCase().includes('AFE')).length;
1300
+ const tot = (document.querySelector('#ov_cost b')?.textContent || document.getElementById('k_total')?.textContent || '').trim();
1301
+ const burn= (document.querySelector('#ov_burn b')?.textContent || document.getElementById('k_avg')?.textContent || '').trim();
1302
+ const hST = (document.getElementById('h_st')?.textContent || '').trim();
1303
+ const hOT = (document.getElementById('h_ot')?.textContent || '').trim();
1304
+ const hDT = (document.getElementById('h_dt')?.textContent || '').trim();
1305
+ const hTOT= (document.getElementById('h_tot')?.textContent || document.getElementById('h_total')?.textContent || '').trim();
1306
+ let y = 76;
1307
+ pdf.text(`Generated: ${now.toLocaleString()}`, M, y); y+=20;
1308
+ if(dr){ pdf.text(`Date Range: ${dr}`, M, y); y+=26; }
1309
+ if(tot){ pdf.text(`Total Cost: ${tot}`, M, y); y+=18; }
1310
+ if(burn){ pdf.text(`Avg Daily Burn: ${burn}`, M, y); y+=18; }
1311
+ pdf.text(`Hours (DFL): ST ${hST} | OT ${hOT} | DT ${hDT} | Total ${hTOT}`, M, y);
1312
+
1313
+ // Helper to add chart by selector
1314
+ async function addChart(title, selector){
1315
+ const dataURL = await snap(selector);
1316
+ if(!dataURL) return;
1317
+ pdf.addPage('a4','landscape');
1318
+ pdf.setFillColor(255,255,255); pdf.rect(0,0,W,H,'F'); pdf.setTextColor(0,0,0);
1319
+ pdf.setFont('helvetica','bold'); pdf.setFontSize(16); pdf.text(title, M, 40);
1320
+ await addImageFit(pdf, dataURL, M, 56, BOXW, BOXH-40);
1321
+ }
1322
+ await addChart('Daily Cost (bars) + Cumulative (line)', '#chartDaily');
1323
+ await addChart('Cumulative Cost', '#chartCum');
1324
+ await addChart('Cost by Module', '#chartModule');
1325
+ await addChart('Top Categories', '#chartTop');
1326
+ await addChart('Contractor Compare', '#chartCompare');
1327
+ await addChart('MAFE vs AFE - Cost Comparison', '#afeMafeCost');
1328
+ await addChart('MAFE vs AFE - Hours Comparison', '#afeMafeHours');
1329
+ // Page numbers
1330
+ const pc = pdf.getNumberOfPages();
1331
+ pdf.setFont('helvetica','normal'); pdf.setFontSize(9);
1332
+ for(let i=1;i<=pc;i++){ pdf.setPage(i); pdf.text(String(i)+' / '+String(pc), W - M, H - 10, {align:'right'}); }
1333
+
1334
+ pdf.save('CostMaster_Report.pdf');
1335
+ }catch(err){ console.error(err); alert('PDF export failed: '+(err?.message||err)); }
1336
+ }
1337
+ document.addEventListener('DOMContentLoaded', ()=>{
1338
+ const btn = document.getElementById('exportPdf');
1339
+ if(btn){ btn.onclick = exportReportPDF; }
1340
+ });
1341
+ })();
1342
+ </script>
1343
+
1344
+
1345
+ <script>
1346
+ (function(){
1347
+ const $ = id => document.getElementById(id);
1348
+ const pick = (...sels)=>sels.map(s=>document.querySelector(s)).find(Boolean);
1349
+ const toNum = v => Number(String(v||'').replace(/[^\d.-]/g,'')) || 0;
1350
+ const usd = n => n.toLocaleString(undefined,{style:'currency',currency:'USD',maximumFractionDigits:0});
1351
+ const parseNum = v => { if(v==null) return 0; const s=(''+v).replace(/[^\d.\-]/g,''); const n=Number(s); return Number.isFinite(n)?n:0; };
1352
+
1353
+ const manualRateEl = $('mc_manualRate');
1354
+ const uncEl = $('mc_uncertainty'), uncLbl=$('mc_uncertaintyLabel');
1355
+ const HEl = $('mc_horizon'), NEl = $('mc_iters');
1356
+ const runBtn = $('mc_runBtn'), exportBtn = $('mc_exportBtn');
1357
+ const kCur=$('mc_kpiCurrent'), k10=$('mc_kpiP10'), k50=$('mc_kpiP50'), k90=$('mc_kpiP90');
1358
+ const winBtns = ['mc_win_30','mc_win_45','mc_win_60'].map(id=>$(id));
1359
+ let activeWindow = 30;
1360
+ winBtns.forEach(b=>b&&b.addEventListener('click',()=>{
1361
+ activeWindow = +b.dataset.window;
1362
+ winBtns.forEach(x=>x.style.opacity = (x===b)? '1' : '.7');
1363
+ }));
1364
+ if(winBtns[0]) winBtns[0].style.opacity='1';
1365
+
1366
+ uncEl.addEventListener('input', ()=> uncLbl.textContent = (uncEl.value|0)+'%');
1367
+
1368
+ function boxMuller(){
1369
+ let u=0,v=0; while(u===0) u=Math.random(); while(v===0) v=Math.random();
1370
+ return Math.sqrt(-2.0*Math.log(u))*Math.cos(2.0*Math.PI*v);
1371
+ }
1372
+ function buildDateLabels(H){
1373
+ const out=[]; const base=new Date();
1374
+ 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'})); }
1375
+ return out;
1376
+ }
1377
+ function getCurrentTotal(){ return toNum(pick('#k_total','#total-cost')?.textContent); }
1378
+ function getAvgBurn(){ return toNum(pick('#k_avg','#avg-daily-burn')?.textContent); }
1379
+
1380
+ const ctx = document.getElementById('mc_fanChart').getContext('2d');
1381
+ let chart = new Chart(ctx, {
1382
+ type:'line',
1383
+ data:{datasets:[]},
1384
+ options:{
1385
+ responsive:true, maintainAspectRatio:false, animation:false,
1386
+ interaction:{mode:'index', intersect:false},
1387
+ plugins:{legend:{display:true, labels:{color:'#cfe0ff'}}},
1388
+ scales:{
1389
+ x:{ticks:{color:'#cfe0ff'}, grid:{color:'rgba(255,255,255,.06)'}},
1390
+ y:{ticks:{color:'#cfe0ff'}, grid:{color:'rgba(255,255,255,.06)'}}
1391
+ }
1392
+ }
1393
+ });
1394
+
1395
+ function exportCSV(labels, base, p10, p50, p90){
1396
+ const header='Date,Baseline,P10,P50,P90\n';
1397
+ 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');
1398
+ const blob=new Blob([header+rows],{type:'text/csv'}); const url=URL.createObjectURL(blob);
1399
+ const a=document.createElement('a'); a.href=url; a.download='mc_cost_projection.csv'; a.click(); URL.revokeObjectURL(url);
1400
+ }
1401
+
1402
+ function runMC(){
1403
+ const baseRate = parseNum(manualRateEl.value) || getAvgBurn();
1404
+ const U = (+uncEl.value||0)/100;
1405
+ const H = Math.max(1, +HEl.value||60);
1406
+ const N = Math.max(100, +NEl.value||5000);
1407
+ const currentTotal = getCurrentTotal();
1408
+
1409
+ if(kCur) kCur.textContent = usd(currentTotal||0);
1410
+
1411
+ if(!(baseRate>0)){
1412
+ chart.data.datasets=[]; chart.update();
1413
+ [k10,k50,k90].forEach(k=>k.textContent='$—');
1414
+ return;
1415
+ }
1416
+
1417
+ const series=[];
1418
+ for(let i=0;i<N;i++){
1419
+ let cum=currentTotal; const path=new Array(H);
1420
+ for(let d=0;d<H;d++){
1421
+ const z=boxMuller();
1422
+ const today=Math.max(0, baseRate + z*(U*baseRate));
1423
+ cum+=today; path[d]=cum;
1424
+ }
1425
+ series.push(path);
1426
+ }
1427
+
1428
+ const p10=[],p50=[],p90=[];
1429
+ for(let d=0;d<H;d++){
1430
+ const col=series.map(r=>r[d]).sort((a,b)=>a-b);
1431
+ p10.push(col[Math.floor(0.10*(N-1))]);
1432
+ p50.push(col[Math.floor(0.50*(N-1))]);
1433
+ p90.push(col[Math.floor(0.90*(N-1))]);
1434
+ }
1435
+
1436
+ if(k10) k10.textContent=usd(p10[H-1]);
1437
+ if(k50) k50.textContent=usd(p50[H-1]);
1438
+ if(k90) k90.textContent=usd(p90[H-1]);
1439
+
1440
+ const labels=buildDateLabels(H);
1441
+ chart.data.datasets=[
1442
+ {label:'P90',data:p90,borderColor:'#f08ac3',backgroundColor:'rgba(240,138,195,.15)',fill:1,tension:.25},
1443
+ {label:'P50',data:p50,borderColor:'#ffc455',backgroundColor:'rgba(255,196,85,.15)',fill:false,tension:.25},
1444
+ {label:'P10',data:p10,borderColor:'#22e1e8',backgroundColor:'rgba(34,225,232,.15)',fill:-1,tension:.25}
1445
+ ];
1446
+ chart.data.labels=labels; chart.update();
1447
+
1448
+ exportBtn.onclick=()=>exportCSV(labels,currentTotal,p10,p50,p90);
1449
+ }
1450
+
1451
+ // initial paint
1452
+ const ct0 = getCurrentTotal();
1453
+ if (kCur && ct0>0) kCur.textContent = usd(ct0);
1454
+ if (!manualRateEl.value) {
1455
+ const b0 = getAvgBurn(); if (b0>0) manualRateEl.value = Math.round(b0);
1456
+ }
1457
+ runBtn.addEventListener('click', runMC);
1458
+
1459
+ // keep synced if overview changes
1460
+ let raf=0; const sync=()=>{ if(raf) cancelAnimationFrame(raf); raf=requestAnimationFrame(()=>{
1461
+ const ct=getCurrentTotal(); if(kCur && ct>0) kCur.textContent=usd(ct);
1462
+ if(!manualRateEl.value){ const b=getAvgBurn(); if(b>0) manualRateEl.value=Math.round(b); }
1463
+ });};
1464
+ ['#k_total','#total-cost','#k_avg','#avg-daily-burn'].forEach(sel=>{
1465
+ const n=document.querySelector(sel); if(n) new MutationObserver(sync).observe(n,{subtree:true,childList:true,characterData:true});
1466
+ });
1467
+ })();
1468
+ </script>
1469
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-qwensite.hf.space/logo.svg" alt="qwensite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-qwensite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >QwenSite</a> - 🧬 <a href="https://enzostvs-qwensite.hf.space?remix=Kaliman-1981/costmaster2-0" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
1470
+ </html>
prompts.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ I'm using the "Rolling Burn + Quick Forecast" dashboard, and I love the current setup for calculating overall averages and projections. However, the daily average burn rate currently pulls from the entire historical data range (e.g., from 2025-02-01 onward in my case), which gives a long-term view but doesn't highlight recent trends. I'd like to request adding a new section, row, or line to the dashboard for "Cost Trends" that calculates and displays rolling average daily burn rates over shorter recent periods. Specifically: Last 7 days average burn Last 14 days average burn Last 21 days average burn Last 30 days average burn Last 45 days average burn These could be shown in a simple table or list below the existing "Rate Window" options (which already include 30d, 45d, 60d). Additionally, for each of these rolling averages, please include a calculation of the projected additional cost by multiplying the average by the number of remaining days to the forecast target date (e.g., in my current setup, that's 62 days to 12/15/2025). Then, display the projected total cost (current total + projected additional) for each window. This would help me quickly compare scenarios based on recent trends vs. the full historical average. For example, if the last 7-day average is $X, the projection would be: $X * 62 days = projected additional, then add to current total for the full forecast. This feature would make the tool even more powerful for spotting short-term changes in burn rates. I've attached a screenshot of my current dashboard for reference.
2
+ I'm using the "Rolling Burn + Quick Forecast" dashboard, and I love the current setup for calculating overall averages and projections. However, the daily average burn rate currently pulls from the entire historical data range (e.g., from 2025-02-01 onward in my case), which gives a long-term view but doesn't highlight recent trends. I'd like to request adding a new section, row, or line to the dashboard for "Cost Trends" that calculates and displays rolling average daily burn rates over shorter recent periods. Specifically: Last 7 days average burn Last 14 days average burn Last 21 days average burn Last 30 days average burn Last 45 days average burn These could be shown in a simple table or list below the existing "Rate Window" options (which already include 30d, 45d, 60d). Additionally, for each of these rolling averages, please include a calculation of the projected additional cost by multiplying the average by the number of remaining days to the forecast target date (e.g., in my current setup, that's 62 days to 12/15/2025). Then, display the projected total cost (current total + projected additional) for each window. This would help me quickly compare scenarios based on recent trends vs. the full historical average. For example, if the last 7-day average is $X, the projection would be: $X * 62 days = projected additional, then add to current total for the full forecast. This feature would make the tool even more powerful for spotting short-term changes in burn rates. I've attached a screenshot of my current dashboard for reference.