HMVL commited on
Commit
028f0c8
·
verified ·
1 Parent(s): a819623

Redesign: amber-phosphor terminal aesthetic (Departure Mono)

Browse files
Files changed (1) hide show
  1. index.html +417 -415
index.html CHANGED
@@ -1,415 +1,417 @@
1
- <title>Engineering Activity Tracker</title>
2
- <meta name="description" content="Interactive engineering activity tracker demo" />
3
- <link rel="preconnect" href="https://fonts.googleapis.com" />
4
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
5
- <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@600;700;800&family=IBM+Plex+Mono:wght@400;500;600&family=Public+Sans:wght@400;500;600;700&display=swap" />
6
- <style>
7
- :root{
8
- --paper:#eef1f5; --grid:rgba(31,92,158,.055); --surface:#ffffff; --surface-2:#f4f7fa;
9
- --ink:#191d23; --ink-2:#525b67; --muted:#89919c; --line:#e2e7ec; --line-2:#d2d9e0;
10
- --accent:#1f5c9e; --accent-2:#17497f; --accent-ink:#ffffff; --accent-wash:#1f5c9e12;
11
- --amber:#b26a12; --good:#2e7d46; --warn:#b26a12; --crit:#c0392b; --defer:#69707a;
12
- --shadow:0 1px 2px rgba(25,29,35,.05), 0 4px 18px rgba(25,29,35,.06);
13
- --shadow-lg:0 24px 60px rgba(25,29,35,.22);
14
- --disp:"Archivo","Segoe UI",system-ui,sans-serif;
15
- --body:"Public Sans",system-ui,-apple-system,"Segoe UI",sans-serif;
16
- --mono:"IBM Plex Mono",ui-monospace,Consolas,monospace;
17
- }
18
- *{box-sizing:border-box}
19
- html{scroll-behavior:smooth}
20
- body{
21
- margin:0; color:var(--ink); font-family:var(--body); font-size:15px; line-height:1.5;
22
- background-color:var(--paper);
23
- background-image:linear-gradient(var(--grid) 1px,transparent 1px),linear-gradient(90deg,var(--grid) 1px,transparent 1px);
24
- background-size:26px 26px;
25
- -webkit-font-smoothing:antialiased;
26
- }
27
- h1,h2,h3{margin:0;font-family:var(--disp);font-weight:700;letter-spacing:-.01em;text-wrap:balance}
28
- button{font-family:inherit;cursor:pointer}
29
- a{color:var(--accent-2)}
30
- :focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:3px}
31
- .wrap{max-width:1160px;margin:0 auto;padding:0 22px}
32
-
33
- /* top bar */
34
- .top{border-bottom:1px solid var(--line);background:rgba(255,255,255,.82);backdrop-filter:saturate(1.3) blur(8px);position:sticky;top:0;z-index:20}
35
- .top-in{display:flex;align-items:center;gap:14px;height:64px}
36
- .mark{width:36px;height:36px;flex:none;border:1.5px solid var(--accent);border-radius:7px;display:grid;place-items:center;color:var(--accent)}
37
- .mark svg{width:20px;height:20px}
38
- .name{font-family:var(--disp);font-weight:800;font-size:17px}
39
- .spec{font-family:var(--mono);font-size:10.5px;letter-spacing:.16em;color:var(--muted);text-transform:uppercase;margin-top:1px}
40
- .top-tag{margin-left:auto;font-family:var(--mono);font-size:11px;color:var(--ink-2);border:1px solid var(--line-2);border-radius:999px;padding:5px 11px;background:var(--surface)}
41
-
42
- main{padding:26px 0 70px}
43
- .eyebrow{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--muted);font-weight:600;display:flex;align-items:center;gap:12px;margin:0 0 14px}
44
- .eyebrow::after{content:"";height:1px;flex:1;background:var(--line)}
45
- .intro h1{font-size:27px}
46
- .intro p{color:var(--ink-2);max-width:66ch;margin:8px 0 0}
47
- .section{margin-top:34px}
48
-
49
- /* KPI */
50
- .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
51
- .kpi{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:15px 17px;box-shadow:var(--shadow);position:relative}
52
- .kpi::before{content:"";position:absolute;left:0;top:11px;bottom:11px;width:3px;border-radius:3px;background:var(--accent)}
53
- .kpi.k-amber::before{background:var(--amber)} .kpi.k-crit::before{background:var(--crit)}
54
- .kpi .lab{font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-weight:600}
55
- .kpi .val{font-family:var(--disp);font-weight:800;font-size:33px;line-height:1.05;margin-top:7px;font-variant-numeric:tabular-nums}
56
- .kpi .val .u{font-size:15px;color:var(--muted);font-weight:600;margin-left:3px}
57
- .kpi .meta{font-size:12.5px;color:var(--ink-2);margin-top:5px}
58
-
59
- /* panels */
60
- .grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
61
- .panel{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:17px 19px 15px;box-shadow:var(--shadow)}
62
- .panel-h{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:12px}
63
- .panel-h h2{font-size:15.5px}
64
- .panel-h .note{font-family:var(--mono);font-size:11px;color:var(--muted)}
65
-
66
- /* charts */
67
- svg.chart{width:100%;height:auto;display:block;font-family:var(--body)}
68
- .bar-lab{fill:var(--ink-2);font-size:12.5px}
69
- .bar-val{fill:var(--ink);font-size:12px;font-weight:600;font-variant-numeric:tabular-nums}
70
- .bar-track{fill:var(--surface-2)}
71
- .bar-fill{fill:var(--accent)}
72
- .bar-fill.alt{fill:#3f7cb8}
73
- .bar g:hover .bar-fill{fill:var(--accent-2)}
74
- .mbar rect{transition:fill .12s}
75
- .month-lab{fill:var(--muted);font-size:11px;font-family:var(--mono)}
76
- .empty{padding:26px;text-align:center;color:var(--muted);font-size:13.5px}
77
-
78
- /* toolbar + table */
79
- .toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:12px}
80
- input,select{font-family:inherit;font-size:13.5px;color:var(--ink);background:var(--surface);border:1px solid var(--line-2);border-radius:8px;padding:8px 11px;width:100%}
81
- input::placeholder{color:var(--muted)}
82
- select{appearance:none;padding-right:28px;background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);background-position:calc(100% - 14px) center,calc(100% - 9px) center;background-size:5px 5px,5px 5px;background-repeat:no-repeat}
83
- .search{min-width:230px}
84
- .spacer{flex:1}
85
- .tscroll{overflow-x:auto;border:1px solid var(--line);border-radius:12px;background:var(--surface);box-shadow:var(--shadow)}
86
- table{width:100%;border-collapse:collapse;font-size:13.5px;min-width:900px}
87
- thead th{text-align:left;padding:11px 14px;font-family:var(--mono);font-size:10.5px;letter-spacing:.09em;text-transform:uppercase;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line);background:var(--surface-2);position:sticky;top:0}
88
- th.num,td.num{text-align:right;font-variant-numeric:tabular-nums}
89
- tbody td{padding:10px 14px;border-bottom:1px solid var(--line);vertical-align:top}
90
- tbody tr:last-child td{border-bottom:0}
91
- tbody tr:hover td{background:var(--surface-2)}
92
- .mono{font-family:var(--mono);font-size:12.5px}
93
- .blocker{display:block;margin-top:3px;font-size:12px;color:var(--crit)}
94
- .chip{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:2px 9px;border-radius:6px;white-space:nowrap;border:1px solid var(--line-2);color:var(--ink-2);background:var(--surface-2)}
95
- .pill{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px;white-space:nowrap}
96
- .pill .dot{width:7px;height:7px;border-radius:50%}
97
- .st-done{color:var(--good);background:color-mix(in srgb,var(--good) 12%,transparent)} .st-done .dot{background:var(--good)}
98
- .st-prog{color:var(--accent);background:var(--accent-wash)} .st-prog .dot{background:var(--accent)}
99
- .st-block{color:var(--crit);background:color-mix(in srgb,var(--crit) 12%,transparent)} .st-block .dot{background:var(--crit)}
100
- .st-defer{color:var(--defer);background:color-mix(in srgb,var(--defer) 12%,transparent)} .st-defer .dot{background:var(--defer)}
101
- .rowbtn{border:1px solid var(--line-2);background:var(--surface);color:var(--ink-2);border-radius:7px;padding:4px 9px;font-size:12px;font-weight:600}
102
- .rowbtn:hover{border-color:var(--crit);color:var(--crit)}
103
-
104
- /* forms */
105
- .card{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:17px 19px;box-shadow:var(--shadow)}
106
- .f2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
107
- .frow{display:flex;flex-direction:column;gap:5px;margin-bottom:11px}
108
- label{font-size:12px;font-weight:600;color:var(--ink-2)}
109
- .hint{color:var(--muted);font-weight:400}
110
- .btn{border:1px solid var(--accent);background:var(--accent);color:var(--accent-ink);border-radius:8px;padding:9px 15px;font-size:13.5px;font-weight:600}
111
- .btn:hover{background:var(--accent-2);border-color:var(--accent-2)}
112
- .btn-ghost{background:var(--surface);color:var(--ink);border-color:var(--line-2)}
113
- .btn-ghost:hover{border-color:var(--accent);background:var(--surface)}
114
- .err{font-size:12.5px;color:var(--crit);min-height:1px}
115
-
116
- /* exports strip */
117
- .exports{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}
118
- .ex{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:15px 17px;box-shadow:var(--shadow)}
119
- .ex h3{font-family:var(--disp);font-size:14.5px;display:flex;align-items:center;gap:9px}
120
- .ex .g{width:30px;height:30px;border-radius:7px;display:grid;place-items:center;color:#fff;font-family:var(--mono);font-weight:600;font-size:11px;flex:none}
121
- .ex p{margin:9px 0 9px;font-size:12.5px;color:var(--ink-2)}
122
- .ex code{font-family:var(--mono);font-size:11.5px;background:var(--surface-2);border:1px solid var(--line);padding:2px 6px;border-radius:5px}
123
-
124
- footer{border-top:1px solid var(--line);margin-top:50px;padding:22px 0 44px;color:var(--muted);font-size:12.5px}
125
- footer .wrap{display:flex;flex-wrap:wrap;gap:6px 18px;align-items:center}
126
- footer b{color:var(--ink-2);font-weight:600}
127
-
128
- /* modal */
129
- .overlay{position:fixed;inset:0;background:rgba(20,26,34,.42);backdrop-filter:blur(3px);display:grid;place-items:center;padding:20px;z-index:40}
130
- .overlay[hidden]{display:none}
131
- .dialog{background:var(--surface);border:1px solid var(--line-2);border-radius:15px;box-shadow:var(--shadow-lg);width:min(560px,100%);max-height:92vh;overflow:auto}
132
- .dialog-h{display:flex;align-items:center;justify-content:space-between;padding:17px 20px;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--surface)}
133
- .dialog-h h2{font-size:16.5px}
134
- .dialog-b{padding:18px 20px}
135
- .dialog-f{display:flex;justify-content:flex-end;gap:10px;padding:15px 20px;border-top:1px solid var(--line)}
136
- .x{border:1px solid var(--line-2);background:var(--surface);border-radius:8px;padding:7px;display:grid;place-items:center}
137
- .full{grid-column:1/-1}
138
- .toast{position:fixed;left:50%;bottom:26px;transform:translateX(-50%) translateY(18px);background:var(--ink);color:#fff;padding:11px 18px;border-radius:9px;font-size:13.5px;box-shadow:var(--shadow-lg);opacity:0;transition:opacity .2s,transform .2s;pointer-events:none;z-index:60}
139
- .toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
140
- .tip{position:fixed;z-index:70;pointer-events:none;background:var(--ink);color:#fff;font-size:12px;padding:6px 9px;border-radius:6px;box-shadow:var(--shadow-lg);opacity:0;transition:opacity .1s;white-space:nowrap}
141
- .tip b{font-family:var(--mono)}
142
-
143
- @media (max-width:880px){.kpis{grid-template-columns:repeat(2,1fr)}.grid2{grid-template-columns:1fr}.exports{grid-template-columns:1fr}.f2{grid-template-columns:1fr}}
144
- @media (prefers-reduced-motion:reduce){*{transition:none!important;scroll-behavior:auto}}
145
- </style>
146
-
147
- <header class="top">
148
- <div class="wrap top-in">
149
- <div class="mark" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M7 14l3-3 3 2 4-6"/></svg></div>
150
- <div>
151
- <div class="name">Engineering Activity Tracker</div>
152
- <div class="spec">Daily log &middot; dashboard &middot; part costs</div>
153
- </div>
154
- <span class="top-tag">Interactive demo</span>
155
- </div>
156
- </header>
157
-
158
- <main class="wrap">
159
- <div class="intro">
160
- <p class="eyebrow">This week &middot; <span id="weekRange"></span></p>
161
- <h1>Day-to-day engineering, logged and rolled up</h1>
162
- <p>A working demo of the tracker: log activities, watch the totals and charts recompute, and keep an eye on hours by project, hours by category, and open blockers. The full tool writes a styled Excel workbook and exports Word and PowerPoint from the same data.</p>
163
- </div>
164
-
165
- <section class="section kpis" id="kpis" aria-label="Key metrics"></section>
166
-
167
- <section class="section grid2">
168
- <div class="panel"><div class="panel-h"><h2>Hours by Project</h2><span class="note">ranked</span></div><div id="projChart"></div></div>
169
- <div class="panel"><div class="panel-h"><h2>Hours by Category</h2><span class="note">ranked</span></div><div id="catChart"></div></div>
170
- </section>
171
-
172
- <section class="section panel"><div class="panel-h"><h2>Hours by Month</h2><span class="note">rollup</span></div><div id="monthChart"></div></section>
173
-
174
- <section class="section">
175
- <p class="eyebrow">Daily Log</p>
176
- <div class="toolbar">
177
- <div><input class="search" id="search" type="search" placeholder="Search description, engineer, ticket..." aria-label="Search the log" /></div>
178
- <select id="fCat" aria-label="Filter by category"><option value="">All categories</option></select>
179
- <select id="fProj" aria-label="Filter by project"><option value="">All projects</option></select>
180
- <select id="fStatus" aria-label="Filter by status"><option value="">All statuses</option></select>
181
- <div class="spacer"></div>
182
- <button class="btn btn-ghost" id="resetBtn">Reset</button>
183
- <button class="btn" id="addBtn">+ Add entry</button>
184
- </div>
185
- <div class="tscroll"><table>
186
- <thead><tr><th>Log ID</th><th>Date</th><th>Engineer</th><th>Project</th><th>Category</th><th>Description</th><th class="num">Hours</th><th>Status</th><th></th></tr></thead>
187
- <tbody id="logBody"></tbody>
188
- </table></div>
189
- </section>
190
-
191
- <section class="section grid2">
192
- <div class="panel">
193
- <div class="panel-h"><h2>Part Costs</h2><button class="btn btn-ghost" id="addPartBtn" style="padding:6px 11px;font-size:12.5px">+ Part</button></div>
194
- <div class="tscroll" style="border:0;box-shadow:none">
195
- <table style="min-width:520px"><thead><tr><th>Part #</th><th>Description</th><th>Project</th><th class="num">Unit</th><th class="num">Qty</th><th class="num">Total</th></tr></thead><tbody id="partsBody"></tbody></table>
196
- </div>
197
- </div>
198
- <div class="panel">
199
- <div class="panel-h"><h2>From the installed tool</h2></div>
200
- <div class="exports" style="grid-template-columns:1fr;gap:11px">
201
- <div class="ex"><h3><span class="g" style="background:#1d6f42">XLS</span> Excel workbook</h3><p>Styled Daily_Log table with native date cells, currency formats, and status conditional formatting, plus dashboard formulas and native charts.</p><code>eng-tracker init</code></div>
202
- <div class="ex"><h3><span class="g" style="background:#2b579a">DOC</span> Word report</h3><p>A weekly report for management: totals, breakdowns, blockers, part costs.</p><code>eng-tracker report</code></div>
203
- <div class="ex"><h3><span class="g" style="background:#c43e1c">PPT</span> PowerPoint deck</h3><p>A status deck with KPI slides and native charts.</p><code>eng-tracker deck</code></div>
204
- </div>
205
- </div>
206
- </section>
207
- </main>
208
-
209
- <footer><div class="wrap">
210
- <span>Interactive demo. Entries you add are saved in this browser only.</span>
211
- <span><b>Stack:</b> Python, openpyxl, pandas, pydantic, Typer, Flask.</span>
212
- </div></footer>
213
-
214
- <div class="overlay" id="entryModal" hidden><div class="dialog" role="dialog" aria-modal="true" aria-labelledby="et">
215
- <div class="dialog-h"><h2 id="et">Add log entry</h2><button class="x" data-close aria-label="Close"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div>
216
- <form id="entryForm" novalidate>
217
- <div class="dialog-b"><div class="f2">
218
- <div class="frow"><label for="e_eng">Engineer</label><input id="e_eng" required /></div>
219
- <div class="frow"><label for="e_date">Date</label><input id="e_date" type="date" required /></div>
220
- <div class="frow"><label for="e_proj">Project</label><input id="e_proj" list="projList" required /><datalist id="projList"></datalist></div>
221
- <div class="frow"><label for="e_cat">Category</label><select id="e_cat"></select></div>
222
- <div class="frow full"><label for="e_desc">Description</label><input id="e_desc" required placeholder="What did you work on?" /></div>
223
- <div class="frow"><label for="e_hours">Hours <span class="hint">(0.25 to 16)</span></label><input id="e_hours" type="number" step="0.25" min="0.25" max="16" required /></div>
224
- <div class="frow"><label for="e_stat">Status</label><select id="e_stat"></select></div>
225
- <div class="frow"><label for="e_ticket">Ticket <span class="hint">(optional)</span></label><input id="e_ticket" placeholder="JIRA-123" /></div>
226
- <div class="frow"><label for="e_block">Blocker <span class="hint">(if Blocked)</span></label><input id="e_block" /></div>
227
- </div><p class="err" id="formErr"></p></div>
228
- <div class="dialog-f"><button type="button" class="btn btn-ghost" data-close>Cancel</button><button type="submit" class="btn">Add entry</button></div>
229
- </form>
230
- </div></div>
231
-
232
- <div class="overlay" id="partModal" hidden><div class="dialog" role="dialog" aria-modal="true" aria-labelledby="pt" style="width:min(460px,100%)">
233
- <div class="dialog-h"><h2 id="pt">Add part</h2><button class="x" data-close aria-label="Close"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div>
234
- <form id="partForm" novalidate>
235
- <div class="dialog-b"><div class="f2">
236
- <div class="frow"><label for="p_num">Part number</label><input id="p_num" required placeholder="P-1001" /></div>
237
- <div class="frow"><label for="p_proj">Project</label><input id="p_proj" list="projList" required /></div>
238
- <div class="frow full"><label for="p_desc">Description</label><input id="p_desc" required /></div>
239
- <div class="frow"><label for="p_cost">Unit cost</label><input id="p_cost" type="number" step="0.01" min="0" required /></div>
240
- <div class="frow"><label for="p_qty">Quantity</label><input id="p_qty" type="number" step="1" min="1" required /></div>
241
- </div><p class="err" id="partErr"></p></div>
242
- <div class="dialog-f"><button type="button" class="btn btn-ghost" data-close>Cancel</button><button type="submit" class="btn">Add part</button></div>
243
- </form>
244
- </div></div>
245
-
246
- <div class="toast" id="toast" role="status"></div>
247
- <div class="tip" id="tip"></div>
248
-
249
- <script>
250
- "use strict";
251
- const CATEGORIES=["Feature","Bugfix","Architecture","Code Review","DevOps","Support","Meeting"];
252
- const STATUSES=["Done","In Progress","Blocked","Deferred"];
253
- const BASE_PROJECTS=["Body Exteriors","Battery Enclosure","Seating Systems","Powertrain","ADAS Cameras","Exterior Mirrors","Internal"];
254
- const STATUS_CLASS={"Done":"st-done","In Progress":"st-prog","Blocked":"st-block","Deferred":"st-defer"};
255
- const LS_KEY="eng-tracker-state-v3";
256
- const TODAY="2026-08-31";
257
-
258
- function mk(date,eng,proj,cat,desc,hrs,status,ticket,blocker){return{date,engineer:eng,project:proj,category:cat,description:desc,hours:hrs,status,ticket:ticket||"",blocker:blocker||""};}
259
- const SEED={
260
- entries:[
261
- mk("2026-06-12","A. Rivera","Body Exteriors","Architecture","Liftgate fixture concept review",4.0,"Done",""),
262
- mk("2026-06-20","A. Rivera","Battery Enclosure","Feature","Coolant bracket rev A model",6.0,"Done","BAT-180"),
263
- mk("2026-06-27","A. Rivera","Seating Systems","Bugfix","Weld sequence correction",2.5,"Done",""),
264
- mk("2026-07-08","A. Rivera","ADAS Cameras","Feature","Front-camera bracket tolerance study",5.0,"Done","ADAS-77"),
265
- mk("2026-07-15","A. Rivera","Powertrain","Code Review","Supplier BOM cost review",2.0,"Done","PR-64"),
266
- mk("2026-07-22","A. Rivera","Internal","Meeting","Monthly production review",1.5,"Done",""),
267
- mk("2026-07-29","A. Rivera","Exterior Mirrors","Support","Line support for mirror actuator",3.0,"Done",""),
268
- mk("2026-08-05","A. Rivera","Body Exteriors","DevOps","Set up shared tracker template",2.5,"Done",""),
269
- mk("2026-08-12","A. Rivera","Battery Enclosure","Feature","Coolant bracket rev B model",6.0,"Done","BAT-214"),
270
- mk("2026-08-19","A. Rivera","ADAS Cameras","Bugfix","Camera mount spec correction",1.5,"Done",""),
271
- mk("2026-08-25","A. Rivera","Body Exteriors","Feature","Part-cost rollup for closure hardware",5.0,"In Progress",""),
272
- mk("2026-08-27","A. Rivera","Seating Systems","Support","Weld matrix walkthrough with techs",1.25,"Done",""),
273
- mk("2026-08-31","A. Rivera","Seating Systems","Bugfix","Root-cause stamping burr on seat bracket",3.0,"Blocked","","Waiting on CMM time to confirm the dimension"),
274
- ],
275
- parts:[
276
- {num:"P-1042",desc:"Front-camera bracket",proj:"ADAS Cameras",cost:42.50,qty:4},
277
- {num:"P-2210",desc:"Coolant bracket, rev B",proj:"Battery Enclosure",cost:8.75,qty:12},
278
- {num:"P-3001",desc:"Liftgate seal clip",proj:"Body Exteriors",cost:0.42,qty:250},
279
- {num:"P-5567",desc:"Weld fixture pin",proj:"Seating Systems",cost:15.00,qty:6},
280
- ],
281
- };
282
-
283
- let state=load();
284
- function load(){try{const r=localStorage.getItem(LS_KEY);if(r)return JSON.parse(r);}catch(e){}return JSON.parse(JSON.stringify(SEED));}
285
- function save(){try{localStorage.setItem(LS_KEY,JSON.stringify(state));}catch(e){}}
286
-
287
- const $=(s,r=document)=>r.querySelector(s);
288
- const $$=(s,r=document)=>[...r.querySelectorAll(s)];
289
- const fmtH=n=>(Math.round(n*100)/100).toFixed(2);
290
- const money=n=>"$"+n.toLocaleString("en-US",{minimumFractionDigits:2,maximumFractionDigits:2});
291
- const esc=s=>String(s).replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
292
- function addDays(iso,n){const d=new Date(iso+"T00:00:00");d.setDate(d.getDate()+n);return d.toISOString().slice(0,10);}
293
- function logIdFor(e){const stamp=e.date.replaceAll("-","");const same=state.entries.filter(x=>x.date===e.date);return `ENG-${stamp}-${String(same.indexOf(e)+1).padStart(4,"0")}`;}
294
- function allProjects(){const s=new Set(BASE_PROJECTS);state.entries.forEach(e=>s.add(e.project));state.parts.forEach(p=>s.add(p.proj));return[...s].sort();}
295
-
296
- function metrics(){
297
- const es=state.entries;
298
- const total=es.reduce((a,e)=>a+e.hours,0);
299
- const wkStart=addDays(TODAY,-6);
300
- const wk=es.filter(e=>e.date>=wkStart&&e.date<=TODAY);
301
- const blocked=es.filter(e=>e.status==="Blocked");
302
- const byCat=CATEGORIES.map(c=>[c,es.filter(e=>e.category===c).reduce((a,e)=>a+e.hours,0)]).filter(x=>x[1]>0).sort((a,b)=>b[1]-a[1]);
303
- const pm={};es.forEach(e=>pm[e.project]=(pm[e.project]||0)+e.hours);
304
- const byProj=Object.entries(pm).sort((a,b)=>b[1]-a[1]);
305
- const mm={};es.forEach(e=>{const m=e.date.slice(0,7);mm[m]=(mm[m]||0)+e.hours;});
306
- const byMonth=Object.entries(mm).sort((a,b)=>a[0]<b[0]?-1:1);
307
- const partCost=state.parts.reduce((a,p)=>a+p.cost*p.qty,0);
308
- return{total,wkStart,wkHours:wk.reduce((a,e)=>a+e.hours,0),wkEntries:wk.length,blocked,byCat,byProj,byMonth,partCost,count:es.length};
309
- }
310
-
311
- function renderKPIs(m){
312
- const t=[
313
- {lab:"Total hours logged",val:fmtH(m.total),u:"h",meta:`${m.count} entries`,cls:""},
314
- {lab:"This week",val:fmtH(m.wkHours),u:"h",meta:`${m.wkEntries} entries`,cls:""},
315
- {lab:"Active blockers",val:String(m.blocked.length),u:"",meta:m.blocked.length?"needs attention":"all clear",cls:m.blocked.length?"k-crit":""},
316
- {lab:"Tracked part cost",val:money(m.partCost),u:"",meta:`${state.parts.length} parts`,cls:"k-amber"},
317
- ];
318
- $("#kpis").innerHTML=t.map(k=>`<div class="kpi ${k.cls}"><div class="lab">${k.lab}</div><div class="val">${k.val}${k.u?`<span class="u">${k.u}</span>`:""}</div><div class="meta">${k.meta}</div></div>`).join("");
319
- }
320
-
321
- function hbars(host,data,alt){
322
- if(!data.length){host.innerHTML=`<div class="empty">No hours logged yet.</div>`;return;}
323
- const W=560,rh=34,top=4,padR=52,labelW=132;
324
- const H=top*2+data.length*rh, tx=labelW, tw=W-tx-padR, max=Math.max(...data.map(d=>d[1]))||1;
325
- let s=`<svg class="chart" viewBox="0 0 ${W} ${H}" role="img" aria-label="bar chart">`;
326
- data.forEach((d,i)=>{const y=top+i*rh,bw=Math.max(3,d[1]/max*tw);
327
- s+=`<g class="bar" data-n="${esc(d[0])}" data-v="${fmtH(d[1])}"><text x="${tx-10}" y="${y+rh/2}" text-anchor="end" dominant-baseline="central" class="bar-lab">${esc(d[0])}</text><rect class="bar-track" x="${tx}" y="${y+8}" width="${tw}" height="${rh-16}" rx="4"/><rect class="bar-fill ${alt?"alt":""}" x="${tx}" y="${y+8}" width="${bw.toFixed(1)}" height="${rh-16}" rx="4"/><text x="${(tx+bw+8).toFixed(1)}" y="${y+rh/2}" dominant-baseline="central" class="bar-val">${fmtH(d[1])}</text></g>`;});
328
- s+="</svg>";host.innerHTML=s;
329
- $$(".bar",host).forEach(g=>{g.addEventListener("mousemove",e=>showTip(e,`<b>${esc(g.dataset.n)}</b>: ${esc(g.dataset.v)} h`));g.addEventListener("mouseleave",hideTip);});
330
- }
331
-
332
- function renderMonth(m){
333
- const host=$("#monthChart"),data=m.byMonth;
334
- if(!data.length){host.innerHTML=`<div class="empty">No hours logged yet.</div>`;return;}
335
- const W=760,H=150,padB=26,padT=10,padL=8,padR=8,n=data.length;
336
- const max=Math.max(...data.map(d=>d[1]))||1, slot=(W-padL-padR)/n, bw=Math.min(70,slot*0.5);
337
- const names={"01":"Jan","02":"Feb","03":"Mar","04":"Apr","05":"May","06":"Jun","07":"Jul","08":"Aug","09":"Sep","10":"Oct","11":"Nov","12":"Dec"};
338
- let s=`<svg class="chart mbar" viewBox="0 0 ${W} ${H}" role="img" aria-label="hours by month">`;
339
- data.forEach((d,i)=>{const cx=padL+slot*i+slot/2,bh=(d[1]/max)*(H-padB-padT-14),x=cx-bw/2,y=H-padB-bh;
340
- const lbl=names[d[0].slice(5,7)]+" "+d[0].slice(2,4);
341
- s+=`<g><rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${bw.toFixed(1)}" height="${bh.toFixed(1)}" rx="4" class="bar-fill"><title>${lbl}: ${fmtH(d[1])} h</title></rect><text x="${cx.toFixed(1)}" y="${(y-5).toFixed(1)}" text-anchor="middle" class="bar-val">${fmtH(d[1])}</text><text x="${cx.toFixed(1)}" y="${H-9}" text-anchor="middle" class="month-lab">${lbl}</text></g>`;});
342
- s+="</svg>";host.innerHTML=s;
343
- }
344
-
345
- function renderLog(){
346
- const q=$("#search").value.trim().toLowerCase(),fc=$("#fCat").value,fp=$("#fProj").value,fs=$("#fStatus").value;
347
- const rows=[...state.entries].map((e,idx)=>({e,idx})).filter(({e})=>(!fc||e.category===fc)&&(!fp||e.project===fp)&&(!fs||e.status===fs)&&(!q||(e.description+" "+e.engineer+" "+e.ticket+" "+e.project).toLowerCase().includes(q))).sort((a,b)=>a.e.date<b.e.date?1:a.e.date>b.e.date?-1:b.idx-a.idx);
348
- const body=$("#logBody");
349
- if(!rows.length){body.innerHTML=`<tr><td colspan="9" class="empty">No entries match these filters.</td></tr>`;return;}
350
- body.innerHTML=rows.map(({e,idx})=>`<tr>
351
- <td class="mono">${esc(logIdFor(e))}</td><td class="mono" style="color:var(--ink-2)">${esc(e.date)}</td><td>${esc(e.engineer)}</td><td>${esc(e.project)}</td>
352
- <td><span class="chip">${esc(e.category)}</span></td>
353
- <td>${esc(e.description)}${e.ticket?` <span class="mono" style="color:var(--muted)">[${esc(e.ticket)}]</span>`:""}${e.status==="Blocked"&&e.blocker?`<span class="blocker">Blocker: ${esc(e.blocker)}</span>`:""}</td>
354
- <td class="num mono">${fmtH(e.hours)}</td>
355
- <td><span class="pill ${STATUS_CLASS[e.status]}"><span class="dot"></span>${esc(e.status)}</span></td>
356
- <td><button class="rowbtn" data-del="${idx}">Delete</button></td></tr>`).join("");
357
- $$("[data-del]",body).forEach(b=>b.addEventListener("click",()=>{const i=+b.dataset.del;const id=logIdFor(state.entries[i]);state.entries.splice(i,1);save();renderAll();refreshOptions();toast(`Deleted ${id}`);}));
358
- }
359
-
360
- function renderParts(){
361
- const body=$("#partsBody");
362
- if(!state.parts.length){body.innerHTML=`<tr><td colspan="6" class="empty">No parts recorded.</td></tr>`;return;}
363
- body.innerHTML=state.parts.map(p=>`<tr><td class="mono">${esc(p.num)}</td><td>${esc(p.desc)}</td><td style="color:var(--ink-2)">${esc(p.proj)}</td><td class="num mono">${money(p.cost)}</td><td class="num mono">${p.qty}</td><td class="num mono" style="font-weight:600">${money(p.cost*p.qty)}</td></tr>`).join("");
364
- }
365
-
366
- function renderAll(){const m=metrics();renderKPIs(m);hbars($("#projChart"),m.byProj,false);hbars($("#catChart"),m.byCat,true);renderMonth(m);renderLog();renderParts();$("#weekRange").textContent=`${m.wkStart} to ${TODAY}`;}
367
-
368
- const tip=$("#tip");
369
- function showTip(e,html){tip.innerHTML=html;tip.style.opacity="1";tip.style.left=(e.clientX+14)+"px";tip.style.top=(e.clientY-10)+"px";}
370
- function hideTip(){tip.style.opacity="0";}
371
-
372
- $$("[data-close]").forEach(b=>b.addEventListener("click",e=>e.target.closest(".overlay").hidden=true));
373
- $$(".overlay").forEach(o=>o.addEventListener("click",e=>{if(e.target===o)o.hidden=true;}));
374
- document.addEventListener("keydown",e=>{if(e.key==="Escape")$$(".overlay").forEach(o=>o.hidden=true);});
375
- function openModal(id){const el=$(id);el.hidden=false;const f=el.querySelector("input,select");if(f)setTimeout(()=>f.focus(),30);}
376
-
377
- ["#search","#fCat","#fProj","#fStatus"].forEach(s=>$(s).addEventListener("input",renderLog));
378
- $("#resetBtn").addEventListener("click",()=>{$("#search").value="";$("#fCat").value="";$("#fProj").value="";$("#fStatus").value="";renderLog();});
379
-
380
- $("#addBtn").addEventListener("click",()=>{$("#entryForm").reset();$("#formErr").textContent="";$("#e_date").value=TODAY;$("#e_eng").value=state.entries[0]?.engineer||"A. Rivera";openModal("#entryModal");});
381
- $("#entryForm").addEventListener("submit",e=>{
382
- e.preventDefault();
383
- const g=id=>$(id).value.trim();
384
- const en={date:g("#e_date"),engineer:g("#e_eng"),project:g("#e_proj"),category:$("#e_cat").value,description:g("#e_desc"),hours:parseFloat($("#e_hours").value),status:$("#e_stat").value,ticket:g("#e_ticket"),blocker:g("#e_block")};
385
- const err=validate(en);
386
- if(err){$("#formErr").textContent=err;return;}
387
- state.entries.push(en);save();renderAll();refreshOptions();$("#entryModal").hidden=true;toast(`Added ${logIdFor(en)}`);
388
- });
389
- function validate(e){
390
- if(!e.engineer)return"Engineer is required.";
391
- if(!e.date)return"Date is required.";
392
- if(!e.project)return"Project is required.";
393
- if(!e.description)return"Description is required.";
394
- if(isNaN(e.hours))return"Hours must be a number.";
395
- if(e.hours<0.25)return"Hours must be at least 0.25.";
396
- if(e.hours>16)return"Hours must not exceed 16.00.";
397
- if(e.status==="Blocked"&&!e.blocker)return"Add blocker details when the status is Blocked.";
398
- return null;
399
- }
400
- $("#addPartBtn").addEventListener("click",()=>{$("#partForm").reset();$("#partErr").textContent="";openModal("#partModal");});
401
- $("#partForm").addEventListener("submit",e=>{
402
- e.preventDefault();
403
- const num=$("#p_num").value.trim(),desc=$("#p_desc").value.trim(),proj=$("#p_proj").value.trim(),cost=parseFloat($("#p_cost").value),qty=parseInt($("#p_qty").value,10);
404
- if(!num||!desc||!proj)return $("#partErr").textContent="Part number, description, and project are required.";
405
- if(isNaN(cost)||cost<0)return $("#partErr").textContent="Unit cost must be zero or more.";
406
- if(isNaN(qty)||qty<1)return $("#partErr").textContent="Quantity must be one or more.";
407
- state.parts.push({num,desc,proj,cost,qty});save();renderAll();refreshOptions();$("#partModal").hidden=true;toast(`Added part ${num}`);
408
- });
409
- let toastT;
410
- function toast(msg){const t=$("#toast");t.textContent=msg;t.classList.add("show");clearTimeout(toastT);toastT=setTimeout(()=>t.classList.remove("show"),2200);}
411
- function fill(sel,items,all){const cur=sel.value;sel.innerHTML=(all?`<option value="">${all}</option>`:"")+items.map(i=>`<option>${esc(i)}</option>`).join("");sel.value=cur;}
412
- function refreshOptions(){fill($("#e_cat"),CATEGORIES,null);fill($("#e_stat"),STATUSES,null);fill($("#fCat"),CATEGORIES,"All categories");fill($("#fStatus"),STATUSES,"All statuses");fill($("#fProj"),allProjects(),"All projects");$("#projList").innerHTML=allProjects().map(p=>`<option value="${esc(p)}">`).join("");}
413
-
414
- refreshOptions();renderAll();
415
- </script>
 
 
 
1
+ <title>Engineering Activity Tracker</title>
2
+ <meta name="description" content="Interactive engineering activity tracker demo" />
3
+ <style>
4
+ @font-face{font-family:'Departure Mono';src:url(data:font/woff2;base64,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) format('woff2');font-weight:400;font-style:normal;font-display:swap}
5
+ :root{
6
+ --bg:#100d09; --panel:#181410; --panel-2:#1e1913;
7
+ --ink:#ecdcc0; --ink-2:#b7a688; --muted:#7c7057; --line:#2c2519; --line-2:#443a29;
8
+ --amber:#ffb000; --amber-b:#ffcc4d; --amber-dim:#9a7a2a; --amber-wash:rgba(255,176,0,.10);
9
+ --green:#5fd97a; --red:#ff5c47; --defer:#9a8f78;
10
+ --glow:0 0 7px rgba(255,176,0,.45);
11
+ --mono:'Departure Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;
12
+ }
13
+ *{box-sizing:border-box}
14
+ html{scroll-behavior:smooth}
15
+ body{
16
+ margin:0; background:var(--bg); color:var(--ink);
17
+ font-family:var(--mono); font-size:14px; line-height:1.6;
18
+ background-image:radial-gradient(130% 90% at 50% -20%, rgba(255,176,0,.07), rgba(255,176,0,0) 55%);
19
+ background-attachment:fixed;
20
+ }
21
+ .crt{position:fixed;inset:0;z-index:9999;pointer-events:none;opacity:.7;
22
+ background:repeating-linear-gradient(to bottom, rgba(0,0,0,0) 0 2px, rgba(0,0,0,.16) 3px 3px)}
23
+ .crt::after{content:"";position:absolute;inset:0;box-shadow:inset 0 0 180px rgba(0,0,0,.55)}
24
+ h1,h2,h3{margin:0;font-family:var(--mono);font-weight:400;letter-spacing:.01em}
25
+ button{font-family:inherit;cursor:pointer}
26
+ a{color:var(--amber-b)}
27
+ ::selection{background:var(--amber);color:var(--bg)}
28
+ :focus-visible{outline:1px solid var(--amber);outline-offset:2px}
29
+ .wrap{max-width:1160px;margin:0 auto;padding:0 22px}
30
+
31
+ @keyframes boot{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
32
+ main>*{animation:boot .5s both}
33
+ main>*:nth-child(1){animation-delay:.04s} main>*:nth-child(2){animation-delay:.12s}
34
+ main>*:nth-child(3){animation-delay:.20s} main>*:nth-child(4){animation-delay:.28s}
35
+ main>*:nth-child(5){animation-delay:.36s} main>*:nth-child(6){animation-delay:.44s}
36
+ .cursor{display:inline-block;width:.55em;height:1em;background:var(--amber);vertical-align:-2px;box-shadow:var(--glow);animation:blink 1.1s steps(1) infinite}
37
+ @keyframes blink{50%{opacity:0}}
38
+
39
+ .top{border-bottom:1px solid var(--line);background:rgba(16,13,9,.82);backdrop-filter:blur(6px);position:sticky;top:0;z-index:30}
40
+ .top-in{display:flex;align-items:center;gap:13px;height:64px}
41
+ .mark{width:34px;height:34px;flex:none;border:1px solid var(--amber);display:grid;place-items:center;color:var(--amber);box-shadow:var(--glow),inset 0 0 8px rgba(255,176,0,.12)}
42
+ .mark svg{width:19px;height:19px}
43
+ .name{font-size:16px;color:var(--amber);text-shadow:var(--glow);letter-spacing:.02em}
44
+ .spec{font-size:11px;color:var(--ink-2);margin-top:2px}
45
+ .top-tag{margin-left:auto;font-size:11px;color:var(--green);border:1px solid var(--line-2);padding:4px 10px;letter-spacing:.08em}
46
+
47
+ main{padding:26px 0 72px}
48
+ .eyebrow{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--amber-dim);display:flex;align-items:center;gap:12px;margin:0 0 14px}
49
+ .eyebrow::before{content:"//";color:var(--amber);opacity:.7}
50
+ .eyebrow::after{content:"";height:1px;flex:1;background:var(--line)}
51
+ .intro h1{font-size:24px;color:var(--ink)}
52
+ .intro p{color:var(--ink-2);max-width:70ch;margin:8px 0 0}
53
+ .section{margin-top:34px}
54
+
55
+ .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
56
+ .kpi{background:var(--panel);border:1px solid var(--line);border-top:2px solid var(--amber);padding:14px 16px}
57
+ .kpi.k-crit{border-top-color:var(--red)} .kpi.k-amber{border-top-color:var(--amber)}
58
+ .kpi .lab{font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
59
+ .kpi .val{font-size:30px;line-height:1.05;margin-top:8px;color:var(--amber);text-shadow:var(--glow);font-variant-numeric:tabular-nums}
60
+ .kpi.k-crit .val{color:var(--red);text-shadow:0 0 7px rgba(255,92,71,.4)}
61
+ .kpi .val .u{font-size:14px;color:var(--muted);text-shadow:none;margin-left:2px}
62
+ .kpi .meta{font-size:12px;color:var(--ink-2);margin-top:5px}
63
+
64
+ .grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
65
+ .panel{background:var(--panel);border:1px solid var(--line);padding:16px 18px 14px;position:relative}
66
+ .panel::before{content:"";position:absolute;top:-1px;left:-1px;width:9px;height:9px;border-top:2px solid var(--amber);border-left:2px solid var(--amber)}
67
+ .panel-h{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:12px}
68
+ .panel-h h2{font-size:14px;color:var(--ink);letter-spacing:.02em}
69
+ .panel-h .note{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.1em}
70
+
71
+ svg.chart{width:100%;height:auto;display:block;font-family:var(--mono)}
72
+ .bar-lab{fill:var(--ink-2);font-size:12px}
73
+ .bar-val{fill:var(--amber-b);font-size:12px;font-variant-numeric:tabular-nums}
74
+ .bar-track{fill:rgba(255,176,0,.06)}
75
+ .bar-fill{fill:var(--amber);filter:drop-shadow(0 0 3px rgba(255,176,0,.55))}
76
+ .bar-fill.alt{fill:var(--green);filter:drop-shadow(0 0 3px rgba(95,217,122,.5))}
77
+ .bar g:hover .bar-fill{fill:var(--amber-b)}
78
+ .month-lab{fill:var(--muted);font-size:11px}
79
+ .empty{padding:26px;text-align:center;color:var(--muted);font-size:13px}
80
+
81
+ .toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:12px}
82
+ input,select{font-family:inherit;font-size:13px;color:var(--ink);background:var(--bg);border:1px solid var(--line-2);padding:8px 11px;width:100%}
83
+ input::placeholder{color:var(--muted)}
84
+ input:focus,select:focus{border-color:var(--amber);outline:none;box-shadow:0 0 0 1px var(--amber-wash)}
85
+ select{appearance:none;padding-right:26px}
86
+ .search{min-width:230px}
87
+ .spacer{flex:1}
88
+ .tscroll{overflow-x:auto;border:1px solid var(--line);background:var(--panel)}
89
+ table{width:100%;border-collapse:collapse;font-size:13px;min-width:900px}
90
+ thead th{text-align:left;padding:10px 13px;font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--amber-dim);border-bottom:1px solid var(--line-2);background:var(--panel-2);position:sticky;top:0}
91
+ th.num,td.num{text-align:right;font-variant-numeric:tabular-nums}
92
+ tbody td{padding:9px 13px;border-bottom:1px solid var(--line);vertical-align:top}
93
+ tbody tr:last-child td{border-bottom:0}
94
+ tbody tr:hover td{background:var(--amber-wash)}
95
+ .mono{font-size:12.5px}
96
+ .blocker{display:block;margin-top:3px;font-size:12px;color:var(--red)}
97
+ .chip{display:inline-flex;align-items:center;font-size:11.5px;padding:1px 8px;border:1px solid var(--line-2);color:var(--ink-2);white-space:nowrap}
98
+ .pill{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;padding:1px 9px;border:1px solid;white-space:nowrap}
99
+ .pill .dot{width:6px;height:6px;border-radius:50%}
100
+ .st-done{color:var(--green);border-color:rgba(95,217,122,.4)} .st-done .dot{background:var(--green);box-shadow:0 0 5px var(--green)}
101
+ .st-prog{color:var(--amber);border-color:rgba(255,176,0,.4)} .st-prog .dot{background:var(--amber);box-shadow:var(--glow)}
102
+ .st-block{color:var(--red);border-color:rgba(255,92,71,.45)} .st-block .dot{background:var(--red);box-shadow:0 0 5px var(--red)}
103
+ .st-defer{color:var(--defer);border-color:var(--line-2)} .st-defer .dot{background:var(--defer)}
104
+ .rowbtn{border:1px solid var(--line-2);background:transparent;color:var(--ink-2);padding:3px 9px;font-size:12px;font-family:inherit}
105
+ .rowbtn:hover{border-color:var(--red);color:var(--red)}
106
+
107
+ .card{background:var(--panel);border:1px solid var(--line);padding:16px 18px}
108
+ .f2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
109
+ .frow{display:flex;flex-direction:column;gap:5px;margin-bottom:11px}
110
+ label{font-size:12px;color:var(--ink-2)}
111
+ .hint{color:var(--muted)}
112
+ .btn{border:1px solid var(--amber);background:transparent;color:var(--amber);padding:8px 15px;font-size:13px;font-family:inherit;letter-spacing:.02em}
113
+ .btn:hover{background:var(--amber);color:var(--bg);box-shadow:var(--glow)}
114
+ .btn-ghost{border-color:var(--line-2);color:var(--ink-2)}
115
+ .btn-ghost:hover{border-color:var(--amber);color:var(--amber);background:transparent;box-shadow:none}
116
+ .err{font-size:12px;color:var(--red);min-height:1px}
117
+
118
+ .exports{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}
119
+ .ex{background:var(--panel);border:1px solid var(--line);padding:14px 16px}
120
+ .ex h3{font-size:13.5px;display:flex;align-items:center;gap:9px;color:var(--ink)}
121
+ .ex .g{width:32px;height:24px;display:grid;place-items:center;color:var(--amber)!important;background:var(--panel-2)!important;border:1px solid var(--line-2);font-size:11px;flex:none}
122
+ .ex p{margin:9px 0 9px;font-size:12px;color:var(--ink-2)}
123
+ .ex code{font-size:11.5px;background:var(--bg);border:1px solid var(--line-2);padding:2px 6px;color:var(--amber-b)}
124
+
125
+ footer{border-top:1px solid var(--line);margin-top:50px;padding:22px 0 44px;color:var(--muted);font-size:12px}
126
+ footer .wrap{display:flex;flex-wrap:wrap;gap:6px 18px;align-items:center}
127
+ footer b{color:var(--ink-2)}
128
+
129
+ .overlay{position:fixed;inset:0;background:rgba(8,6,3,.66);backdrop-filter:blur(2px);display:grid;place-items:center;padding:20px;z-index:100}
130
+ .overlay[hidden]{display:none}
131
+ .dialog{background:var(--panel);border:1px solid var(--amber);box-shadow:0 0 40px rgba(255,176,0,.12),0 24px 60px rgba(0,0,0,.6);width:min(560px,100%);max-height:92vh;overflow:auto}
132
+ .dialog-h{display:flex;align-items:center;justify-content:space-between;padding:15px 18px;border-bottom:1px solid var(--line-2);position:sticky;top:0;background:var(--panel)}
133
+ .dialog-h h2{font-size:15px;color:var(--amber)}
134
+ .dialog-b{padding:16px 18px}
135
+ .dialog-f{display:flex;justify-content:flex-end;gap:10px;padding:14px 18px;border-top:1px solid var(--line-2)}
136
+ .x{border:1px solid var(--line-2);background:transparent;color:var(--ink-2);padding:6px;display:grid;place-items:center}
137
+ .x:hover{border-color:var(--amber);color:var(--amber)}
138
+ .full{grid-column:1/-1}
139
+ .toast{position:fixed;left:50%;bottom:26px;transform:translateX(-50%) translateY(18px);background:var(--bg);border:1px solid var(--amber);color:var(--amber);padding:10px 18px;font-size:13px;box-shadow:var(--glow);opacity:0;transition:opacity .2s,transform .2s;pointer-events:none;z-index:200}
140
+ .toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
141
+ .tip{position:fixed;z-index:300;pointer-events:none;background:var(--bg);border:1px solid var(--amber);color:var(--ink);font-size:12px;padding:5px 9px;box-shadow:var(--glow);opacity:0;transition:opacity .1s;white-space:nowrap}
142
+ .tip b{color:var(--amber)}
143
+
144
+ @media (max-width:880px){.kpis{grid-template-columns:repeat(2,1fr)}.grid2{grid-template-columns:1fr}.exports{grid-template-columns:1fr}.f2{grid-template-columns:1fr}}
145
+ @media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important;scroll-behavior:auto}}
146
+ </style>
147
+
148
+ <div class="crt" aria-hidden="true"></div>
149
+ <header class="top">
150
+ <div class="wrap top-in">
151
+ <div class="mark" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M7 14l3-3 3 2 4-6"/></svg></div>
152
+ <div>
153
+ <div class="name">Engineering Activity Tracker</div>
154
+ <div class="spec">magna@eng-tracker:~$ ./tracker --live <span class="cursor"></span></div>
155
+ </div>
156
+ <span class="top-tag">&#9679; LIVE</span>
157
+ </div>
158
+ </header>
159
+
160
+ <main class="wrap">
161
+ <div class="intro">
162
+ <p class="eyebrow">This week &middot; <span id="weekRange"></span></p>
163
+ <h1>Day-to-day engineering, logged and rolled up</h1>
164
+ <p>A working demo of the tracker: log activities, watch the totals and charts recompute, and keep an eye on hours by project, hours by category, and open blockers. The full tool writes a styled Excel workbook and exports Word and PowerPoint from the same data.</p>
165
+ </div>
166
+
167
+ <section class="section kpis" id="kpis" aria-label="Key metrics"></section>
168
+
169
+ <section class="section grid2">
170
+ <div class="panel"><div class="panel-h"><h2>Hours by Project</h2><span class="note">ranked</span></div><div id="projChart"></div></div>
171
+ <div class="panel"><div class="panel-h"><h2>Hours by Category</h2><span class="note">ranked</span></div><div id="catChart"></div></div>
172
+ </section>
173
+
174
+ <section class="section panel"><div class="panel-h"><h2>Hours by Month</h2><span class="note">rollup</span></div><div id="monthChart"></div></section>
175
+
176
+ <section class="section">
177
+ <p class="eyebrow">Daily Log</p>
178
+ <div class="toolbar">
179
+ <div><input class="search" id="search" type="search" placeholder="Search description, engineer, ticket..." aria-label="Search the log" /></div>
180
+ <select id="fCat" aria-label="Filter by category"><option value="">All categories</option></select>
181
+ <select id="fProj" aria-label="Filter by project"><option value="">All projects</option></select>
182
+ <select id="fStatus" aria-label="Filter by status"><option value="">All statuses</option></select>
183
+ <div class="spacer"></div>
184
+ <button class="btn btn-ghost" id="resetBtn">Reset</button>
185
+ <button class="btn" id="addBtn">+ Add entry</button>
186
+ </div>
187
+ <div class="tscroll"><table>
188
+ <thead><tr><th>Log ID</th><th>Date</th><th>Engineer</th><th>Project</th><th>Category</th><th>Description</th><th class="num">Hours</th><th>Status</th><th></th></tr></thead>
189
+ <tbody id="logBody"></tbody>
190
+ </table></div>
191
+ </section>
192
+
193
+ <section class="section grid2">
194
+ <div class="panel">
195
+ <div class="panel-h"><h2>Part Costs</h2><button class="btn btn-ghost" id="addPartBtn" style="padding:6px 11px;font-size:12.5px">+ Part</button></div>
196
+ <div class="tscroll" style="border:0">
197
+ <table style="min-width:520px"><thead><tr><th>Part #</th><th>Description</th><th>Project</th><th class="num">Unit</th><th class="num">Qty</th><th class="num">Total</th></tr></thead><tbody id="partsBody"></tbody></table>
198
+ </div>
199
+ </div>
200
+ <div class="panel">
201
+ <div class="panel-h"><h2>From the installed tool</h2></div>
202
+ <div class="exports" style="grid-template-columns:1fr;gap:11px">
203
+ <div class="ex"><h3><span class="g">XLS</span> Excel workbook</h3><p>Styled Daily_Log table with native date cells, currency formats, and status conditional formatting, plus dashboard formulas and native charts.</p><code>eng-tracker init</code></div>
204
+ <div class="ex"><h3><span class="g">DOC</span> Word report</h3><p>A weekly report for management: totals, breakdowns, blockers, part costs.</p><code>eng-tracker report</code></div>
205
+ <div class="ex"><h3><span class="g">PPT</span> PowerPoint deck</h3><p>A status deck with KPI slides and native charts.</p><code>eng-tracker deck</code></div>
206
+ </div>
207
+ </div>
208
+ </section>
209
+ </main>
210
+
211
+ <footer><div class="wrap">
212
+ <span>Interactive demo. Entries you add are saved in this browser only.</span>
213
+ <span><b>Stack:</b> Python, openpyxl, pandas, pydantic, Typer, Flask.</span>
214
+ </div></footer>
215
+
216
+ <div class="overlay" id="entryModal" hidden><div class="dialog" role="dialog" aria-modal="true" aria-labelledby="et">
217
+ <div class="dialog-h"><h2 id="et">Add log entry</h2><button class="x" data-close aria-label="Close"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div>
218
+ <form id="entryForm" novalidate>
219
+ <div class="dialog-b"><div class="f2">
220
+ <div class="frow"><label for="e_eng">Engineer</label><input id="e_eng" required /></div>
221
+ <div class="frow"><label for="e_date">Date</label><input id="e_date" type="date" required /></div>
222
+ <div class="frow"><label for="e_proj">Project</label><input id="e_proj" list="projList" required /><datalist id="projList"></datalist></div>
223
+ <div class="frow"><label for="e_cat">Category</label><select id="e_cat"></select></div>
224
+ <div class="frow full"><label for="e_desc">Description</label><input id="e_desc" required placeholder="What did you work on?" /></div>
225
+ <div class="frow"><label for="e_hours">Hours <span class="hint">(0.25 to 16)</span></label><input id="e_hours" type="number" step="0.25" min="0.25" max="16" required /></div>
226
+ <div class="frow"><label for="e_stat">Status</label><select id="e_stat"></select></div>
227
+ <div class="frow"><label for="e_ticket">Ticket <span class="hint">(optional)</span></label><input id="e_ticket" placeholder="JIRA-123" /></div>
228
+ <div class="frow"><label for="e_block">Blocker <span class="hint">(if Blocked)</span></label><input id="e_block" /></div>
229
+ </div><p class="err" id="formErr"></p></div>
230
+ <div class="dialog-f"><button type="button" class="btn btn-ghost" data-close>Cancel</button><button type="submit" class="btn">Add entry</button></div>
231
+ </form>
232
+ </div></div>
233
+
234
+ <div class="overlay" id="partModal" hidden><div class="dialog" role="dialog" aria-modal="true" aria-labelledby="pt" style="width:min(460px,100%)">
235
+ <div class="dialog-h"><h2 id="pt">Add part</h2><button class="x" data-close aria-label="Close"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg></button></div>
236
+ <form id="partForm" novalidate>
237
+ <div class="dialog-b"><div class="f2">
238
+ <div class="frow"><label for="p_num">Part number</label><input id="p_num" required placeholder="P-1001" /></div>
239
+ <div class="frow"><label for="p_proj">Project</label><input id="p_proj" list="projList" required /></div>
240
+ <div class="frow full"><label for="p_desc">Description</label><input id="p_desc" required /></div>
241
+ <div class="frow"><label for="p_cost">Unit cost</label><input id="p_cost" type="number" step="0.01" min="0" required /></div>
242
+ <div class="frow"><label for="p_qty">Quantity</label><input id="p_qty" type="number" step="1" min="1" required /></div>
243
+ </div><p class="err" id="partErr"></p></div>
244
+ <div class="dialog-f"><button type="button" class="btn btn-ghost" data-close>Cancel</button><button type="submit" class="btn">Add part</button></div>
245
+ </form>
246
+ </div></div>
247
+
248
+ <div class="toast" id="toast" role="status"></div>
249
+ <div class="tip" id="tip"></div>
250
+
251
+ <script>
252
+ "use strict";
253
+ const CATEGORIES=["Feature","Bugfix","Architecture","Code Review","DevOps","Support","Meeting"];
254
+ const STATUSES=["Done","In Progress","Blocked","Deferred"];
255
+ const BASE_PROJECTS=["Body Exteriors","Battery Enclosure","Seating Systems","Powertrain","ADAS Cameras","Exterior Mirrors","Internal"];
256
+ const STATUS_CLASS={"Done":"st-done","In Progress":"st-prog","Blocked":"st-block","Deferred":"st-defer"};
257
+ const LS_KEY="eng-tracker-state-v3";
258
+ const TODAY="2026-08-31";
259
+
260
+ function mk(date,eng,proj,cat,desc,hrs,status,ticket,blocker){return{date,engineer:eng,project:proj,category:cat,description:desc,hours:hrs,status,ticket:ticket||"",blocker:blocker||""};}
261
+ const SEED={
262
+ entries:[
263
+ mk("2026-06-12","A. Rivera","Body Exteriors","Architecture","Liftgate fixture concept review",4.0,"Done",""),
264
+ mk("2026-06-20","A. Rivera","Battery Enclosure","Feature","Coolant bracket rev A model",6.0,"Done","BAT-180"),
265
+ mk("2026-06-27","A. Rivera","Seating Systems","Bugfix","Weld sequence correction",2.5,"Done",""),
266
+ mk("2026-07-08","A. Rivera","ADAS Cameras","Feature","Front-camera bracket tolerance study",5.0,"Done","ADAS-77"),
267
+ mk("2026-07-15","A. Rivera","Powertrain","Code Review","Supplier BOM cost review",2.0,"Done","PR-64"),
268
+ mk("2026-07-22","A. Rivera","Internal","Meeting","Monthly production review",1.5,"Done",""),
269
+ mk("2026-07-29","A. Rivera","Exterior Mirrors","Support","Line support for mirror actuator",3.0,"Done",""),
270
+ mk("2026-08-05","A. Rivera","Body Exteriors","DevOps","Set up shared tracker template",2.5,"Done",""),
271
+ mk("2026-08-12","A. Rivera","Battery Enclosure","Feature","Coolant bracket rev B model",6.0,"Done","BAT-214"),
272
+ mk("2026-08-19","A. Rivera","ADAS Cameras","Bugfix","Camera mount spec correction",1.5,"Done",""),
273
+ mk("2026-08-25","A. Rivera","Body Exteriors","Feature","Part-cost rollup for closure hardware",5.0,"In Progress",""),
274
+ mk("2026-08-27","A. Rivera","Seating Systems","Support","Weld matrix walkthrough with techs",1.25,"Done",""),
275
+ mk("2026-08-31","A. Rivera","Seating Systems","Bugfix","Root-cause stamping burr on seat bracket",3.0,"Blocked","","Waiting on CMM time to confirm the dimension"),
276
+ ],
277
+ parts:[
278
+ {num:"P-1042",desc:"Front-camera bracket",proj:"ADAS Cameras",cost:42.50,qty:4},
279
+ {num:"P-2210",desc:"Coolant bracket, rev B",proj:"Battery Enclosure",cost:8.75,qty:12},
280
+ {num:"P-3001",desc:"Liftgate seal clip",proj:"Body Exteriors",cost:0.42,qty:250},
281
+ {num:"P-5567",desc:"Weld fixture pin",proj:"Seating Systems",cost:15.00,qty:6},
282
+ ],
283
+ };
284
+
285
+ let state=load();
286
+ function load(){try{const r=localStorage.getItem(LS_KEY);if(r)return JSON.parse(r);}catch(e){}return JSON.parse(JSON.stringify(SEED));}
287
+ function save(){try{localStorage.setItem(LS_KEY,JSON.stringify(state));}catch(e){}}
288
+
289
+ const $=(s,r=document)=>r.querySelector(s);
290
+ const $$=(s,r=document)=>[...r.querySelectorAll(s)];
291
+ const fmtH=n=>(Math.round(n*100)/100).toFixed(2);
292
+ const money=n=>"$"+n.toLocaleString("en-US",{minimumFractionDigits:2,maximumFractionDigits:2});
293
+ const esc=s=>String(s).replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
294
+ function addDays(iso,n){const d=new Date(iso+"T00:00:00");d.setDate(d.getDate()+n);return d.toISOString().slice(0,10);}
295
+ function logIdFor(e){const stamp=e.date.replaceAll("-","");const same=state.entries.filter(x=>x.date===e.date);return `ENG-${stamp}-${String(same.indexOf(e)+1).padStart(4,"0")}`;}
296
+ function allProjects(){const s=new Set(BASE_PROJECTS);state.entries.forEach(e=>s.add(e.project));state.parts.forEach(p=>s.add(p.proj));return[...s].sort();}
297
+
298
+ function metrics(){
299
+ const es=state.entries;
300
+ const total=es.reduce((a,e)=>a+e.hours,0);
301
+ const wkStart=addDays(TODAY,-6);
302
+ const wk=es.filter(e=>e.date>=wkStart&&e.date<=TODAY);
303
+ const blocked=es.filter(e=>e.status==="Blocked");
304
+ const byCat=CATEGORIES.map(c=>[c,es.filter(e=>e.category===c).reduce((a,e)=>a+e.hours,0)]).filter(x=>x[1]>0).sort((a,b)=>b[1]-a[1]);
305
+ const pm={};es.forEach(e=>pm[e.project]=(pm[e.project]||0)+e.hours);
306
+ const byProj=Object.entries(pm).sort((a,b)=>b[1]-a[1]);
307
+ const mm={};es.forEach(e=>{const m=e.date.slice(0,7);mm[m]=(mm[m]||0)+e.hours;});
308
+ const byMonth=Object.entries(mm).sort((a,b)=>a[0]<b[0]?-1:1);
309
+ const partCost=state.parts.reduce((a,p)=>a+p.cost*p.qty,0);
310
+ return{total,wkStart,wkHours:wk.reduce((a,e)=>a+e.hours,0),wkEntries:wk.length,blocked,byCat,byProj,byMonth,partCost,count:es.length};
311
+ }
312
+
313
+ function renderKPIs(m){
314
+ const t=[
315
+ {lab:"Total hours logged",val:fmtH(m.total),u:"h",meta:`${m.count} entries`,cls:""},
316
+ {lab:"This week",val:fmtH(m.wkHours),u:"h",meta:`${m.wkEntries} entries`,cls:""},
317
+ {lab:"Active blockers",val:String(m.blocked.length),u:"",meta:m.blocked.length?"needs attention":"all clear",cls:m.blocked.length?"k-crit":""},
318
+ {lab:"Tracked part cost",val:money(m.partCost),u:"",meta:`${state.parts.length} parts`,cls:"k-amber"},
319
+ ];
320
+ $("#kpis").innerHTML=t.map(k=>`<div class="kpi ${k.cls}"><div class="lab">${k.lab}</div><div class="val">${k.val}${k.u?`<span class="u">${k.u}</span>`:""}</div><div class="meta">${k.meta}</div></div>`).join("");
321
+ }
322
+
323
+ function hbars(host,data,alt){
324
+ if(!data.length){host.innerHTML=`<div class="empty">No hours logged yet.</div>`;return;}
325
+ const W=560,rh=34,top=4,padR=52,labelW=132;
326
+ const H=top*2+data.length*rh, tx=labelW, tw=W-tx-padR, max=Math.max(...data.map(d=>d[1]))||1;
327
+ let s=`<svg class="chart" viewBox="0 0 ${W} ${H}" role="img" aria-label="bar chart">`;
328
+ data.forEach((d,i)=>{const y=top+i*rh,bw=Math.max(3,d[1]/max*tw);
329
+ s+=`<g class="bar" data-n="${esc(d[0])}" data-v="${fmtH(d[1])}"><text x="${tx-10}" y="${y+rh/2}" text-anchor="end" dominant-baseline="central" class="bar-lab">${esc(d[0])}</text><rect class="bar-track" x="${tx}" y="${y+8}" width="${tw}" height="${rh-16}" rx="4"/><rect class="bar-fill ${alt?"alt":""}" x="${tx}" y="${y+8}" width="${bw.toFixed(1)}" height="${rh-16}" rx="4"/><text x="${(tx+bw+8).toFixed(1)}" y="${y+rh/2}" dominant-baseline="central" class="bar-val">${fmtH(d[1])}</text></g>`;});
330
+ s+="</svg>";host.innerHTML=s;
331
+ $$(".bar",host).forEach(g=>{g.addEventListener("mousemove",e=>showTip(e,`<b>${esc(g.dataset.n)}</b>: ${esc(g.dataset.v)} h`));g.addEventListener("mouseleave",hideTip);});
332
+ }
333
+
334
+ function renderMonth(m){
335
+ const host=$("#monthChart"),data=m.byMonth;
336
+ if(!data.length){host.innerHTML=`<div class="empty">No hours logged yet.</div>`;return;}
337
+ const W=760,H=150,padB=26,padT=10,padL=8,padR=8,n=data.length;
338
+ const max=Math.max(...data.map(d=>d[1]))||1, slot=(W-padL-padR)/n, bw=Math.min(70,slot*0.5);
339
+ const names={"01":"Jan","02":"Feb","03":"Mar","04":"Apr","05":"May","06":"Jun","07":"Jul","08":"Aug","09":"Sep","10":"Oct","11":"Nov","12":"Dec"};
340
+ let s=`<svg class="chart mbar" viewBox="0 0 ${W} ${H}" role="img" aria-label="hours by month">`;
341
+ data.forEach((d,i)=>{const cx=padL+slot*i+slot/2,bh=(d[1]/max)*(H-padB-padT-14),x=cx-bw/2,y=H-padB-bh;
342
+ const lbl=names[d[0].slice(5,7)]+" "+d[0].slice(2,4);
343
+ s+=`<g><rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${bw.toFixed(1)}" height="${bh.toFixed(1)}" rx="4" class="bar-fill"><title>${lbl}: ${fmtH(d[1])} h</title></rect><text x="${cx.toFixed(1)}" y="${(y-5).toFixed(1)}" text-anchor="middle" class="bar-val">${fmtH(d[1])}</text><text x="${cx.toFixed(1)}" y="${H-9}" text-anchor="middle" class="month-lab">${lbl}</text></g>`;});
344
+ s+="</svg>";host.innerHTML=s;
345
+ }
346
+
347
+ function renderLog(){
348
+ const q=$("#search").value.trim().toLowerCase(),fc=$("#fCat").value,fp=$("#fProj").value,fs=$("#fStatus").value;
349
+ const rows=[...state.entries].map((e,idx)=>({e,idx})).filter(({e})=>(!fc||e.category===fc)&&(!fp||e.project===fp)&&(!fs||e.status===fs)&&(!q||(e.description+" "+e.engineer+" "+e.ticket+" "+e.project).toLowerCase().includes(q))).sort((a,b)=>a.e.date<b.e.date?1:a.e.date>b.e.date?-1:b.idx-a.idx);
350
+ const body=$("#logBody");
351
+ if(!rows.length){body.innerHTML=`<tr><td colspan="9" class="empty">No entries match these filters.</td></tr>`;return;}
352
+ body.innerHTML=rows.map(({e,idx})=>`<tr>
353
+ <td class="mono">${esc(logIdFor(e))}</td><td class="mono" style="color:var(--ink-2)">${esc(e.date)}</td><td>${esc(e.engineer)}</td><td>${esc(e.project)}</td>
354
+ <td><span class="chip">${esc(e.category)}</span></td>
355
+ <td>${esc(e.description)}${e.ticket?` <span class="mono" style="color:var(--muted)">[${esc(e.ticket)}]</span>`:""}${e.status==="Blocked"&&e.blocker?`<span class="blocker">Blocker: ${esc(e.blocker)}</span>`:""}</td>
356
+ <td class="num mono">${fmtH(e.hours)}</td>
357
+ <td><span class="pill ${STATUS_CLASS[e.status]}"><span class="dot"></span>${esc(e.status)}</span></td>
358
+ <td><button class="rowbtn" data-del="${idx}">Delete</button></td></tr>`).join("");
359
+ $$("[data-del]",body).forEach(b=>b.addEventListener("click",()=>{const i=+b.dataset.del;const id=logIdFor(state.entries[i]);state.entries.splice(i,1);save();renderAll();refreshOptions();toast(`Deleted ${id}`);}));
360
+ }
361
+
362
+ function renderParts(){
363
+ const body=$("#partsBody");
364
+ if(!state.parts.length){body.innerHTML=`<tr><td colspan="6" class="empty">No parts recorded.</td></tr>`;return;}
365
+ body.innerHTML=state.parts.map(p=>`<tr><td class="mono">${esc(p.num)}</td><td>${esc(p.desc)}</td><td style="color:var(--ink-2)">${esc(p.proj)}</td><td class="num mono">${money(p.cost)}</td><td class="num mono">${p.qty}</td><td class="num mono" style="font-weight:600">${money(p.cost*p.qty)}</td></tr>`).join("");
366
+ }
367
+
368
+ function renderAll(){const m=metrics();renderKPIs(m);hbars($("#projChart"),m.byProj,false);hbars($("#catChart"),m.byCat,true);renderMonth(m);renderLog();renderParts();$("#weekRange").textContent=`${m.wkStart} to ${TODAY}`;}
369
+
370
+ const tip=$("#tip");
371
+ function showTip(e,html){tip.innerHTML=html;tip.style.opacity="1";tip.style.left=(e.clientX+14)+"px";tip.style.top=(e.clientY-10)+"px";}
372
+ function hideTip(){tip.style.opacity="0";}
373
+
374
+ $$("[data-close]").forEach(b=>b.addEventListener("click",e=>e.target.closest(".overlay").hidden=true));
375
+ $$(".overlay").forEach(o=>o.addEventListener("click",e=>{if(e.target===o)o.hidden=true;}));
376
+ document.addEventListener("keydown",e=>{if(e.key==="Escape")$$(".overlay").forEach(o=>o.hidden=true);});
377
+ function openModal(id){const el=$(id);el.hidden=false;const f=el.querySelector("input,select");if(f)setTimeout(()=>f.focus(),30);}
378
+
379
+ ["#search","#fCat","#fProj","#fStatus"].forEach(s=>$(s).addEventListener("input",renderLog));
380
+ $("#resetBtn").addEventListener("click",()=>{$("#search").value="";$("#fCat").value="";$("#fProj").value="";$("#fStatus").value="";renderLog();});
381
+
382
+ $("#addBtn").addEventListener("click",()=>{$("#entryForm").reset();$("#formErr").textContent="";$("#e_date").value=TODAY;$("#e_eng").value=state.entries[0]?.engineer||"A. Rivera";openModal("#entryModal");});
383
+ $("#entryForm").addEventListener("submit",e=>{
384
+ e.preventDefault();
385
+ const g=id=>$(id).value.trim();
386
+ const en={date:g("#e_date"),engineer:g("#e_eng"),project:g("#e_proj"),category:$("#e_cat").value,description:g("#e_desc"),hours:parseFloat($("#e_hours").value),status:$("#e_stat").value,ticket:g("#e_ticket"),blocker:g("#e_block")};
387
+ const err=validate(en);
388
+ if(err){$("#formErr").textContent=err;return;}
389
+ state.entries.push(en);save();renderAll();refreshOptions();$("#entryModal").hidden=true;toast(`Added ${logIdFor(en)}`);
390
+ });
391
+ function validate(e){
392
+ if(!e.engineer)return"Engineer is required.";
393
+ if(!e.date)return"Date is required.";
394
+ if(!e.project)return"Project is required.";
395
+ if(!e.description)return"Description is required.";
396
+ if(isNaN(e.hours))return"Hours must be a number.";
397
+ if(e.hours<0.25)return"Hours must be at least 0.25.";
398
+ if(e.hours>16)return"Hours must not exceed 16.00.";
399
+ if(e.status==="Blocked"&&!e.blocker)return"Add blocker details when the status is Blocked.";
400
+ return null;
401
+ }
402
+ $("#addPartBtn").addEventListener("click",()=>{$("#partForm").reset();$("#partErr").textContent="";openModal("#partModal");});
403
+ $("#partForm").addEventListener("submit",e=>{
404
+ e.preventDefault();
405
+ const num=$("#p_num").value.trim(),desc=$("#p_desc").value.trim(),proj=$("#p_proj").value.trim(),cost=parseFloat($("#p_cost").value),qty=parseInt($("#p_qty").value,10);
406
+ if(!num||!desc||!proj)return $("#partErr").textContent="Part number, description, and project are required.";
407
+ if(isNaN(cost)||cost<0)return $("#partErr").textContent="Unit cost must be zero or more.";
408
+ if(isNaN(qty)||qty<1)return $("#partErr").textContent="Quantity must be one or more.";
409
+ state.parts.push({num,desc,proj,cost,qty});save();renderAll();refreshOptions();$("#partModal").hidden=true;toast(`Added part ${num}`);
410
+ });
411
+ let toastT;
412
+ function toast(msg){const t=$("#toast");t.textContent=msg;t.classList.add("show");clearTimeout(toastT);toastT=setTimeout(()=>t.classList.remove("show"),2200);}
413
+ function fill(sel,items,all){const cur=sel.value;sel.innerHTML=(all?`<option value="">${all}</option>`:"")+items.map(i=>`<option>${esc(i)}</option>`).join("");sel.value=cur;}
414
+ function refreshOptions(){fill($("#e_cat"),CATEGORIES,null);fill($("#e_stat"),STATUSES,null);fill($("#fCat"),CATEGORIES,"All categories");fill($("#fStatus"),STATUSES,"All statuses");fill($("#fProj"),allProjects(),"All projects");$("#projList").innerHTML=allProjects().map(p=>`<option value="${esc(p)}">`).join("");}
415
+
416
+ refreshOptions();renderAll();
417
+ </script>