HMVL commited on
Commit
281ee28
·
verified ·
1 Parent(s): deedc46

Add interactive demo

Browse files
Files changed (1) hide show
  1. index.html +415 -19
index.html CHANGED
@@ -1,19 +1,415 @@
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
+ <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, 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>