Download index.html from HMVL/engineering-activity-tracker: direct link, hf CLI and curl.
- Browser
- Download file 32.1 kB
-
https://huggingface.co/spaces/HMVL/engineering-activity-tracker/resolve/main/index.html
- Command line
-
hf download hf://spaces/HMVL/engineering-activity-tracker/index.html
-
curl -L -o index.html https://huggingface.co/spaces/HMVL/engineering-activity-tracker/resolve/main/index.html
32.1 kB
| <title>Engineering Activity Tracker</title> | |
| <meta name="description" content="Interactive engineering activity tracker demo" /> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | |
| <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap" /> | |
| <style> | |
| :root{ | |
| --bg:#100d09; --panel:#181410; --panel-2:#1e1913; | |
| --ink:#f4e9d2; --ink-2:#cdbd9a; --muted:#8d8168; --line:#2c2519; --line-2:#443a29; | |
| --amber:#ffb000; --amber-b:#ffcc4d; --amber-dim:#9a7a2a; --amber-wash:rgba(255,176,0,.10); | |
| --green:#5fd97a; --red:#ff5c47; --defer:#9a8f78; | |
| --glow:0 0 7px rgba(255,176,0,.45); | |
| --mono:'Space Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace; | |
| } | |
| *{box-sizing:border-box} | |
| html{scroll-behavior:smooth} | |
| body{ | |
| margin:0; background:var(--bg); color:var(--ink); | |
| font-family:var(--mono); font-size:15px; line-height:1.7; | |
| -webkit-font-smoothing:antialiased; | |
| background-image:radial-gradient(130% 90% at 50% -20%, rgba(255,176,0,.07), rgba(255,176,0,0) 55%); | |
| background-attachment:fixed; | |
| } | |
| .crt{position:fixed;inset:0;z-index:9999;pointer-events:none;opacity:.35; | |
| background:repeating-linear-gradient(to bottom, rgba(0,0,0,0) 0 3px, rgba(0,0,0,.12) 4px 4px)} | |
| .crt::after{content:"";position:absolute;inset:0;box-shadow:inset 0 0 180px rgba(0,0,0,.55)} | |
| h1,h2,h3{margin:0;font-family:var(--mono);font-weight:400;letter-spacing:.01em} | |
| button{font-family:inherit;cursor:pointer} | |
| a{color:var(--amber-b)} | |
| ::selection{background:var(--amber);color:var(--bg)} | |
| :focus-visible{outline:1px solid var(--amber);outline-offset:2px} | |
| .wrap{max-width:1160px;margin:0 auto;padding:0 22px} | |
| @keyframes boot{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}} | |
| main>*{animation:boot .5s both} | |
| main>*:nth-child(1){animation-delay:.04s} main>*:nth-child(2){animation-delay:.12s} | |
| main>*:nth-child(3){animation-delay:.20s} main>*:nth-child(4){animation-delay:.28s} | |
| main>*:nth-child(5){animation-delay:.36s} main>*:nth-child(6){animation-delay:.44s} | |
| .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} | |
| @keyframes blink{50%{opacity:0}} | |
| .top{border-bottom:1px solid var(--line);background:rgba(16,13,9,.82);backdrop-filter:blur(6px);position:sticky;top:0;z-index:30} | |
| .top-in{display:flex;align-items:center;gap:13px;height:64px} | |
| .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)} | |
| .mark svg{width:19px;height:19px} | |
| .name{font-size:16px;color:var(--amber);text-shadow:var(--glow);letter-spacing:.02em} | |
| .spec{font-size:11px;color:var(--ink-2);margin-top:2px} | |
| .top-tag{margin-left:auto;font-size:11px;color:var(--green);border:1px solid var(--line-2);padding:4px 10px;letter-spacing:.08em} | |
| main{padding:26px 0 72px} | |
| .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} | |
| .eyebrow::before{content:"//";color:var(--amber);opacity:.7} | |
| .eyebrow::after{content:"";height:1px;flex:1;background:var(--line)} | |
| .intro h1{font-size:24px;color:var(--ink)} | |
| .intro p{color:var(--ink-2);max-width:70ch;margin:8px 0 0} | |
| .section{margin-top:34px} | |
| .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px} | |
| .kpi{background:var(--panel);border:1px solid var(--line);border-top:2px solid var(--amber);padding:14px 16px} | |
| .kpi.k-crit{border-top-color:var(--red)} .kpi.k-amber{border-top-color:var(--amber)} | |
| .kpi .lab{font-size:11.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-2)} | |
| .kpi .val{font-size:30px;line-height:1.05;margin-top:8px;color:var(--amber);text-shadow:var(--glow);font-variant-numeric:tabular-nums} | |
| .kpi.k-crit .val{color:var(--red);text-shadow:0 0 7px rgba(255,92,71,.4)} | |
| .kpi .val .u{font-size:14px;color:var(--muted);text-shadow:none;margin-left:2px} | |
| .kpi .meta{font-size:12px;color:var(--ink-2);margin-top:5px} | |
| .grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px} | |
| .panel{background:var(--panel);border:1px solid var(--line);padding:16px 18px 14px;position:relative} | |
| .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)} | |
| .panel-h{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:12px} | |
| .panel-h h2{font-size:14px;color:var(--ink);letter-spacing:.02em} | |
| .panel-h .note{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.1em} | |
| svg.chart{width:100%;height:auto;display:block;font-family:var(--mono)} | |
| .bar-lab{fill:var(--ink-2);font-size:12.5px} | |
| .bar-val{fill:var(--amber-b);font-size:12.5px;font-variant-numeric:tabular-nums} | |
| .bar-track{fill:rgba(255,176,0,.06)} | |
| .bar-fill{fill:var(--amber);filter:drop-shadow(0 0 3px rgba(255,176,0,.55))} | |
| .bar-fill.alt{fill:var(--green);filter:drop-shadow(0 0 3px rgba(95,217,122,.5))} | |
| .bar g:hover .bar-fill{fill:var(--amber-b)} | |
| .month-lab{fill:var(--muted);font-size:11px} | |
| .empty{padding:26px;text-align:center;color:var(--muted);font-size:13px} | |
| .toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:12px} | |
| 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%} | |
| input::placeholder{color:var(--muted)} | |
| input:focus,select:focus{border-color:var(--amber);outline:none;box-shadow:0 0 0 1px var(--amber-wash)} | |
| select{appearance:none;padding-right:26px} | |
| .search{min-width:230px} | |
| .spacer{flex:1} | |
| .tscroll{overflow-x:auto;border:1px solid var(--line);background:var(--panel)} | |
| table{width:100%;border-collapse:collapse;font-size:14px;min-width:900px} | |
| thead th{text-align:left;padding:10px 13px;font-size:11.5px;letter-spacing:.09em;text-transform:uppercase;color:var(--amber-dim);border-bottom:1px solid var(--line-2);background:var(--panel-2);position:sticky;top:0} | |
| th.num,td.num{text-align:right;font-variant-numeric:tabular-nums} | |
| tbody td{padding:9px 13px;border-bottom:1px solid var(--line);vertical-align:top} | |
| tbody tr:last-child td{border-bottom:0} | |
| tbody tr:hover td{background:var(--amber-wash)} | |
| tbody tr.r-done{opacity:.5} tbody tr.r-done:hover{opacity:.72} | |
| .mono{font-size:13.5px} | |
| .blocker{display:block;margin-top:3px;font-size:12.5px;color:var(--red)} | |
| .chip{display:inline-flex;align-items:center;font-size:12px;padding:2px 8px;border:1px solid var(--line-2);color:var(--ink-2);white-space:nowrap} | |
| .pill{display:inline-flex;align-items:center;gap:6px;font-size:12px;padding:2px 9px;border:1px solid;white-space:nowrap} | |
| .pill .dot{width:6px;height:6px;border-radius:50%} | |
| .st-done{color:var(--defer);border-color:var(--line-2)} .st-done .dot{background:var(--defer);box-shadow:none} | |
| .st-prog{color:var(--amber);border-color:rgba(255,176,0,.4)} .st-prog .dot{background:var(--amber);box-shadow:var(--glow)} | |
| .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)} | |
| .st-defer{color:var(--defer);border-color:var(--line-2)} .st-defer .dot{background:var(--defer)} | |
| .rowbtn{border:1px solid var(--line-2);background:transparent;color:var(--ink-2);padding:3px 9px;font-size:12px;font-family:inherit} | |
| .rowbtn:hover{border-color:var(--red);color:var(--red)} | |
| .card{background:var(--panel);border:1px solid var(--line);padding:16px 18px} | |
| .f2{display:grid;grid-template-columns:1fr 1fr;gap:12px} | |
| .frow{display:flex;flex-direction:column;gap:5px;margin-bottom:11px} | |
| label{font-size:12px;color:var(--ink-2)} | |
| .hint{color:var(--muted)} | |
| .btn{border:1px solid var(--amber);background:transparent;color:var(--amber);padding:8px 15px;font-size:13px;font-family:inherit;letter-spacing:.02em} | |
| .btn:hover{background:var(--amber);color:var(--bg);box-shadow:var(--glow)} | |
| .btn-ghost{border-color:var(--line-2);color:var(--ink-2)} | |
| .btn-ghost:hover{border-color:var(--amber);color:var(--amber);background:transparent;box-shadow:none} | |
| .err{font-size:12px;color:var(--red);min-height:1px} | |
| .exports{display:grid;grid-template-columns:repeat(3,1fr);gap:14px} | |
| .ex{background:var(--panel);border:1px solid var(--line);padding:14px 16px} | |
| .ex h3{font-size:13.5px;display:flex;align-items:center;gap:9px;color:var(--ink)} | |
| .ex .g{width:32px;height:24px;display:grid;place-items:center;color:var(--amber);background:var(--panel-2);border:1px solid var(--line-2);font-size:11px;flex:none} | |
| .ex p{margin:9px 0 9px;font-size:12px;color:var(--ink-2)} | |
| .ex code{font-size:11.5px;background:var(--bg);border:1px solid var(--line-2);padding:2px 6px;color:var(--amber-b)} | |
| footer{border-top:1px solid var(--line);margin-top:50px;padding:22px 0 44px;color:var(--muted);font-size:12px} | |
| footer .wrap{display:flex;flex-wrap:wrap;gap:6px 18px;align-items:center} | |
| footer b{color:var(--ink-2)} | |
| .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} | |
| .overlay[hidden]{display:none} | |
| .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} | |
| .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)} | |
| .dialog-h h2{font-size:15px;color:var(--amber)} | |
| .dialog-b{padding:16px 18px} | |
| .dialog-f{display:flex;justify-content:flex-end;gap:10px;padding:14px 18px;border-top:1px solid var(--line-2)} | |
| .x{border:1px solid var(--line-2);background:transparent;color:var(--ink-2);padding:6px;display:grid;place-items:center} | |
| .x:hover{border-color:var(--amber);color:var(--amber)} | |
| .full{grid-column:1/-1} | |
| .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} | |
| .toast.show{opacity:1;transform:translateX(-50%) translateY(0)} | |
| .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} | |
| .tip b{color:var(--amber)} | |
| @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}} | |
| @media (prefers-reduced-motion:reduce){*{animation:none;transition:none;scroll-behavior:auto}} | |
| </style> | |
| <div class="crt" aria-hidden="true"></div> | |
| <header class="top"> | |
| <div class="wrap top-in"> | |
| <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> | |
| <div> | |
| <div class="name">Engineering Activity Tracker</div> | |
| <div class="spec">magna@eng-tracker:~$ ./tracker --live <span class="cursor"></span></div> | |
| </div> | |
| <span class="top-tag">● LIVE</span> | |
| </div> | |
| </header> | |
| <main class="wrap"> | |
| <div class="intro"> | |
| <p class="eyebrow">This week · <span id="weekRange"></span></p> | |
| <h1>Day-to-day engineering, logged and rolled up</h1> | |
| <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> | |
| </div> | |
| <section class="section kpis" id="kpis" aria-label="Key metrics"></section> | |
| <section class="section grid2"> | |
| <div class="panel"><div class="panel-h"><h2>Hours by Project</h2><span class="note">ranked</span></div><div id="projChart"></div></div> | |
| <div class="panel"><div class="panel-h"><h2>Hours by Category</h2><span class="note">ranked</span></div><div id="catChart"></div></div> | |
| </section> | |
| <section class="section panel"><div class="panel-h"><h2>Hours by Month</h2><span class="note">rollup</span></div><div id="monthChart"></div></section> | |
| <section class="section"> | |
| <p class="eyebrow">Daily Log</p> | |
| <div class="toolbar"> | |
| <div><input class="search" id="search" type="search" placeholder="Search description, engineer, ticket..." aria-label="Search the log" /></div> | |
| <select id="fCat" aria-label="Filter by category"><option value="">All categories</option></select> | |
| <select id="fProj" aria-label="Filter by project"><option value="">All projects</option></select> | |
| <select id="fStatus" aria-label="Filter by status"><option value="">All statuses</option></select> | |
| <div class="spacer"></div> | |
| <button class="btn btn-ghost" id="resetBtn">Reset</button> | |
| <button class="btn" id="addBtn">+ Add entry</button> | |
| </div> | |
| <div class="tscroll"><table> | |
| <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> | |
| <tbody id="logBody"></tbody> | |
| </table></div> | |
| </section> | |
| <section class="section grid2"> | |
| <div class="panel"> | |
| <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> | |
| <div class="tscroll" style="border:0"> | |
| <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> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-h"><h2>From the installed tool</h2></div> | |
| <div class="exports" style="grid-template-columns:1fr;gap:11px"> | |
| <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> | |
| <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> | |
| <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> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <footer><div class="wrap"> | |
| <span>Interactive demo. Entries you add are saved in this browser only.</span> | |
| <span><b>Stack:</b> Python, openpyxl, pandas, pydantic, Typer, Flask.</span> | |
| </div></footer> | |
| <div class="overlay" id="entryModal" hidden><div class="dialog" role="dialog" aria-modal="true" aria-labelledby="et"> | |
| <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> | |
| <form id="entryForm" novalidate> | |
| <div class="dialog-b"><div class="f2"> | |
| <div class="frow"><label for="e_eng">Engineer</label><input id="e_eng" required /></div> | |
| <div class="frow"><label for="e_date">Date</label><input id="e_date" type="date" required /></div> | |
| <div class="frow"><label for="e_proj">Project</label><input id="e_proj" list="projList" required /><datalist id="projList"></datalist></div> | |
| <div class="frow"><label for="e_cat">Category</label><select id="e_cat"></select></div> | |
| <div class="frow full"><label for="e_desc">Description</label><input id="e_desc" required placeholder="What did you work on?" /></div> | |
| <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> | |
| <div class="frow"><label for="e_stat">Status</label><select id="e_stat"></select></div> | |
| <div class="frow"><label for="e_ticket">Ticket <span class="hint">(optional)</span></label><input id="e_ticket" placeholder="JIRA-123" /></div> | |
| <div class="frow"><label for="e_block">Blocker <span class="hint">(if Blocked)</span></label><input id="e_block" /></div> | |
| </div><p class="err" id="formErr"></p></div> | |
| <div class="dialog-f"><button type="button" class="btn btn-ghost" data-close>Cancel</button><button type="submit" class="btn">Add entry</button></div> | |
| </form> | |
| </div></div> | |
| <div class="overlay" id="partModal" hidden><div class="dialog" role="dialog" aria-modal="true" aria-labelledby="pt" style="width:min(460px,100%)"> | |
| <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> | |
| <form id="partForm" novalidate> | |
| <div class="dialog-b"><div class="f2"> | |
| <div class="frow"><label for="p_num">Part number</label><input id="p_num" required placeholder="P-1001" /></div> | |
| <div class="frow"><label for="p_proj">Project</label><input id="p_proj" list="projList" required /></div> | |
| <div class="frow full"><label for="p_desc">Description</label><input id="p_desc" required /></div> | |
| <div class="frow"><label for="p_cost">Unit cost</label><input id="p_cost" type="number" step="0.01" min="0" required /></div> | |
| <div class="frow"><label for="p_qty">Quantity</label><input id="p_qty" type="number" step="1" min="1" required /></div> | |
| </div><p class="err" id="partErr"></p></div> | |
| <div class="dialog-f"><button type="button" class="btn btn-ghost" data-close>Cancel</button><button type="submit" class="btn">Add part</button></div> | |
| </form> | |
| </div></div> | |
| <div class="toast" id="toast" role="status"></div> | |
| <div class="tip" id="tip"></div> | |
| <script> | |
| ; | |
| const CATEGORIES=["Feature","Bugfix","Architecture","Code Review","DevOps","Support","Meeting"]; | |
| const STATUSES=["Done","In Progress","Blocked","Deferred"]; | |
| const BASE_PROJECTS=["Body Exteriors","Battery Enclosure","Seating Systems","Powertrain","ADAS Cameras","Exterior Mirrors","Internal"]; | |
| const STATUS_CLASS={"Done":"st-done","In Progress":"st-prog","Blocked":"st-block","Deferred":"st-defer"}; | |
| const STATUS_PRIORITY={"Blocked":0,"In Progress":1,"Deferred":2,"Done":3}; | |
| const LS_KEY="eng-tracker-state-v3"; | |
| const TODAY="2026-08-31"; | |
| 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||""};} | |
| const SEED={ | |
| entries:[ | |
| mk("2026-06-12","A. Rivera","Body Exteriors","Architecture","Liftgate fixture concept review",4.0,"Done",""), | |
| mk("2026-06-20","A. Rivera","Battery Enclosure","Feature","Coolant bracket rev A model",6.0,"Done","BAT-180"), | |
| mk("2026-06-27","A. Rivera","Seating Systems","Bugfix","Weld sequence correction",2.5,"Done",""), | |
| mk("2026-07-08","A. Rivera","ADAS Cameras","Feature","Front-camera bracket tolerance study",5.0,"Done","ADAS-77"), | |
| mk("2026-07-15","A. Rivera","Powertrain","Code Review","Supplier BOM cost review",2.0,"Done","PR-64"), | |
| mk("2026-07-22","A. Rivera","Internal","Meeting","Monthly production review",1.5,"Done",""), | |
| mk("2026-07-29","A. Rivera","Exterior Mirrors","Support","Line support for mirror actuator",3.0,"Done",""), | |
| mk("2026-08-05","A. Rivera","Body Exteriors","DevOps","Set up shared tracker template",2.5,"Done",""), | |
| mk("2026-08-12","A. Rivera","Battery Enclosure","Feature","Coolant bracket rev B model",6.0,"Done","BAT-214"), | |
| mk("2026-08-19","A. Rivera","ADAS Cameras","Bugfix","Camera mount spec correction",1.5,"Done",""), | |
| mk("2026-08-25","A. Rivera","Body Exteriors","Feature","Part-cost rollup for closure hardware",5.0,"In Progress",""), | |
| mk("2026-08-27","A. Rivera","Seating Systems","Support","Weld matrix walkthrough with techs",1.25,"Done",""), | |
| 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"), | |
| ], | |
| parts:[ | |
| {num:"P-1042",desc:"Front-camera bracket",proj:"ADAS Cameras",cost:42.50,qty:4}, | |
| {num:"P-2210",desc:"Coolant bracket, rev B",proj:"Battery Enclosure",cost:8.75,qty:12}, | |
| {num:"P-3001",desc:"Liftgate seal clip",proj:"Body Exteriors",cost:0.42,qty:250}, | |
| {num:"P-5567",desc:"Weld fixture pin",proj:"Seating Systems",cost:15.00,qty:6}, | |
| ], | |
| }; | |
| let state=load(); | |
| function load(){try{const r=localStorage.getItem(LS_KEY);if(r)return JSON.parse(r);}catch(e){}return JSON.parse(JSON.stringify(SEED));} | |
| function save(){try{localStorage.setItem(LS_KEY,JSON.stringify(state));}catch(e){}} | |
| const $=(s,r=document)=>r.querySelector(s); | |
| const $$=(s,r=document)=>[...r.querySelectorAll(s)]; | |
| const fmtH=n=>(Math.round(n*100)/100).toFixed(2); | |
| const money=n=>"$"+n.toLocaleString("en-US",{minimumFractionDigits:2,maximumFractionDigits:2}); | |
| const esc=s=>String(s).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); | |
| function addDays(iso,n){const d=new Date(iso+"T00:00:00Z");d.setUTCDate(d.getUTCDate()+n);return d.toISOString().slice(0,10);} | |
| 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")}`;} | |
| 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();} | |
| function metrics(){ | |
| const es=state.entries; | |
| const total=es.reduce((a,e)=>a+e.hours,0); | |
| const wkStart=addDays(TODAY,-6); | |
| const wk=es.filter(e=>e.date>=wkStart&&e.date<=TODAY); | |
| const blocked=es.filter(e=>e.status==="Blocked"); | |
| 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]); | |
| const pm={};es.forEach(e=>pm[e.project]=(pm[e.project]||0)+e.hours); | |
| const byProj=Object.entries(pm).sort((a,b)=>b[1]-a[1]); | |
| const mm={};es.forEach(e=>{const m=e.date.slice(0,7);mm[m]=(mm[m]||0)+e.hours;}); | |
| const byMonth=Object.entries(mm).sort((a,b)=>a[0]<b[0]?-1:1); | |
| const partCost=state.parts.reduce((a,p)=>a+p.cost*p.qty,0); | |
| return{total,wkStart,wkHours:wk.reduce((a,e)=>a+e.hours,0),wkEntries:wk.length,blocked,byCat,byProj,byMonth,partCost,count:es.length}; | |
| } | |
| function renderKPIs(m){ | |
| const t=[ | |
| {lab:"Total hours logged",val:fmtH(m.total),u:"h",meta:`${m.count} entries`,cls:""}, | |
| {lab:"This week",val:fmtH(m.wkHours),u:"h",meta:`${m.wkEntries} entries`,cls:""}, | |
| {lab:"Active blockers",val:String(m.blocked.length),u:"",meta:m.blocked.length?"needs attention":"all clear",cls:m.blocked.length?"k-crit":""}, | |
| {lab:"Tracked part cost",val:money(m.partCost),u:"",meta:`${state.parts.length} parts`,cls:"k-amber"}, | |
| ]; | |
| $("#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(""); | |
| } | |
| function hbars(host,data,alt){ | |
| if(!data.length){host.innerHTML=`<div class="empty">No hours logged yet.</div>`;return;} | |
| const W=560,rh=34,top=4,padR=52,labelW=132; | |
| const H=top*2+data.length*rh, tx=labelW, tw=W-tx-padR, max=Math.max(...data.map(d=>d[1]))||1; | |
| let s=`<svg class="chart" viewBox="0 0 ${W} ${H}" role="img" aria-label="bar chart">`; | |
| data.forEach((d,i)=>{const y=top+i*rh,bw=Math.max(3,d[1]/max*tw); | |
| 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>`;}); | |
| s+="</svg>";host.innerHTML=s; | |
| $$(".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);}); | |
| } | |
| function renderMonth(m){ | |
| const host=$("#monthChart"),data=m.byMonth; | |
| if(!data.length){host.innerHTML=`<div class="empty">No hours logged yet.</div>`;return;} | |
| const W=760,H=150,padB=26,padT=10,padL=8,padR=8,n=data.length; | |
| const max=Math.max(...data.map(d=>d[1]))||1, slot=(W-padL-padR)/n, bw=Math.min(70,slot*0.5); | |
| 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"}; | |
| let s=`<svg class="chart mbar" viewBox="0 0 ${W} ${H}" role="img" aria-label="hours by month">`; | |
| 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; | |
| const lbl=names[d[0].slice(5,7)]+" "+d[0].slice(2,4); | |
| 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>`;}); | |
| s+="</svg>";host.innerHTML=s; | |
| } | |
| function renderLog(){ | |
| const q=$("#search").value.trim().toLowerCase(),fc=$("#fCat").value,fp=$("#fProj").value,fs=$("#fStatus").value; | |
| 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)=>{const pa=STATUS_PRIORITY[a.e.status]??2,pb=STATUS_PRIORITY[b.e.status]??2;if(pa!==pb)return pa-pb;return a.e.date<b.e.date?1:a.e.date>b.e.date?-1:b.idx-a.idx;}); | |
| const body=$("#logBody"); | |
| if(!rows.length){body.innerHTML=`<tr><td colspan="9" class="empty">No entries match these filters.</td></tr>`;return;} | |
| body.innerHTML=rows.map(({e,idx})=>`<tr class="${e.status==='Done'?'r-done':''}"> | |
| <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> | |
| <td><span class="chip">${esc(e.category)}</span></td> | |
| <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> | |
| <td class="num mono">${fmtH(e.hours)}</td> | |
| <td><span class="pill ${STATUS_CLASS[e.status]}"><span class="dot"></span>${esc(e.status)}</span></td> | |
| <td><button class="rowbtn" data-del="${idx}">Delete</button></td></tr>`).join(""); | |
| $$("[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}`);})); | |
| } | |
| function renderParts(){ | |
| const body=$("#partsBody"); | |
| if(!state.parts.length){body.innerHTML=`<tr><td colspan="6" class="empty">No parts recorded.</td></tr>`;return;} | |
| 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(""); | |
| } | |
| 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}`;} | |
| const tip=$("#tip"); | |
| 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";} | |
| function hideTip(){tip.style.opacity="0";} | |
| $$("[data-close]").forEach(b=>b.addEventListener("click",e=>e.target.closest(".overlay").hidden=true)); | |
| $$(".overlay").forEach(o=>o.addEventListener("click",e=>{if(e.target===o)o.hidden=true;})); | |
| document.addEventListener("keydown",e=>{if(e.key==="Escape")$$(".overlay").forEach(o=>o.hidden=true);}); | |
| function openModal(id){const el=$(id);el.hidden=false;const f=el.querySelector("input,select");if(f)setTimeout(()=>f.focus(),30);} | |
| ["#search","#fCat","#fProj","#fStatus"].forEach(s=>$(s).addEventListener("input",renderLog)); | |
| $("#resetBtn").addEventListener("click",()=>{$("#search").value="";$("#fCat").value="";$("#fProj").value="";$("#fStatus").value="";renderLog();}); | |
| $("#addBtn").addEventListener("click",()=>{$("#entryForm").reset();$("#formErr").textContent="";$("#e_date").value=TODAY;$("#e_eng").value=state.entries[0]?.engineer||"A. Rivera";openModal("#entryModal");}); | |
| $("#entryForm").addEventListener("submit",e=>{ | |
| e.preventDefault(); | |
| const g=id=>$(id).value.trim(); | |
| 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")}; | |
| const err=validate(en); | |
| if(err){$("#formErr").textContent=err;return;} | |
| state.entries.push(en);save();renderAll();refreshOptions();$("#entryModal").hidden=true;toast(`Added ${logIdFor(en)}`); | |
| }); | |
| function validate(e){ | |
| if(!e.engineer)return"Engineer is required."; | |
| if(!e.date)return"Date is required."; | |
| if(!e.project)return"Project is required."; | |
| if(!e.description)return"Description is required."; | |
| if(isNaN(e.hours))return"Hours must be a number."; | |
| if(e.hours<0.25)return"Hours must be at least 0.25."; | |
| if(e.hours>16)return"Hours must not exceed 16.00."; | |
| if(e.status==="Blocked"&&!e.blocker)return"Add blocker details when the status is Blocked."; | |
| return null; | |
| } | |
| $("#addPartBtn").addEventListener("click",()=>{$("#partForm").reset();$("#partErr").textContent="";openModal("#partModal");}); | |
| $("#partForm").addEventListener("submit",e=>{ | |
| e.preventDefault(); | |
| 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); | |
| if(!num||!desc||!proj)return $("#partErr").textContent="Part number, description, and project are required."; | |
| if(isNaN(cost)||cost<0)return $("#partErr").textContent="Unit cost must be zero or more."; | |
| if(isNaN(qty)||qty<1)return $("#partErr").textContent="Quantity must be one or more."; | |
| state.parts.push({num,desc,proj,cost,qty});save();renderAll();refreshOptions();$("#partModal").hidden=true;toast(`Added part ${num}`); | |
| }); | |
| let toastT; | |
| function toast(msg){const t=$("#toast");t.textContent=msg;t.classList.add("show");clearTimeout(toastT);toastT=setTimeout(()=>t.classList.remove("show"),2200);} | |
| 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;} | |
| 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("");} | |
| refreshOptions();renderAll(); | |
| </script> | |