Add interactive demo
Browse files- index.html +415 -19
index.html
CHANGED
|
@@ -1,19 +1,415 @@
|
|
| 1 |
-
<
|
| 2 |
-
<
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 · dashboard · 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 · <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=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[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>
|