Own it · example handover · app/
src/ui/rebuilt.tsx
← All files · the demo this builds
/**
* The "after": a small job-tracking app over the same rows. Same job
* numbers, same names, same dollars as the workbook — visibly aligned on
* the wipe. Each wound has its resolution here: real columns instead of
* merged headers, computed totals instead of typed ones, one date format,
* Mike's orphaned jobs surfaced with a recovered badge, statuses
* normalized, PO a first-class field.
*
* Edits are client-side state that resets on reload — enough to prove
* it's an app, not a screenshot.
*/
import { useState } from "preact/hooks";
import { fmtUSD, toISO, type CleanStatus, type GsmData, type GsmJob } from "@portfolio/fabricator";
export type AppView = "jobs" | "schedule";
export type AppFocus = "total" | "recovered" | "dates" | "po" | null;
const STATUSES: CleanStatus[] = ["Scheduled", "In progress", "Done", "Invoiced", "On hold"];
function shortDate(d: Date): string {
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}
export function Rebuilt({ data, view, onView, focus }: {
data: GsmData;
view: AppView;
onView: (v: AppView) => void;
focus: AppFocus;
}) {
const [open, setOpen] = useState<number | null>(null);
const [statusEdits, setStatusEdits] = useState<ReadonlyMap<number, CleanStatus>>(new Map());
// the total's provenance receipt (spec Half B: "where each number came
// from" on totals) — the old workbook's Total row was typed and wrong,
// so this one shows its arithmetic on demand
const [whence, setWhence] = useState(false);
const jobs: GsmJob[] = [...data.jobs, ...data.mikesJobs].sort(
(a, b) => b.start.getTime() - a.start.getTime() || a.jobNo - b.jobNo,
);
const statusOf = (j: GsmJob) => statusEdits.get(j.jobNo) ?? j.status;
const active = jobs.filter((j) => !j.cancelled);
const totalQuotedC = active.reduce((s, j) => s + j.quotedC, 0);
const cancelledCount = jobs.length - active.length;
const recoveredCount = active.filter((j) => j.fromMikesCopy).length;
const biggest = [...active].sort((a, b) => b.quotedC - a.quotedC).slice(0, 3);
const restC = totalQuotedC - biggest.reduce((s, j) => s + j.quotedC, 0);
const days = ["Mon", "Tue", "Wed", "Thu", "Fri"] as const;
return (
<div class="ssr-app">
<header class="ssr-app-head">
<p class="ssr-app-brand">{data.company} · job board</p>
<nav class="ssr-app-tabs" aria-label="App views">
<button type="button" class={view === "jobs" ? "is-on" : ""} onClick={() => onView("jobs")}>Jobs</button>
<button type="button" class={view === "schedule" ? "is-on" : ""} onClick={() => onView("schedule")}>
Schedule · wk of {shortDate(data.weekOf)}
</button>
</nav>
</header>
{view === "jobs" ? (
<>
<ul class="ssr-jobs">
{jobs.map((j) => (
<li key={j.jobNo} class={`ssr-jobrow ${j.fromMikesCopy && focus === "recovered" ? "is-focus" : ""}`}>
<button type="button" class="ssr-jobbtn" aria-expanded={open === j.jobNo} onClick={() => setOpen(open === j.jobNo ? null : j.jobNo)}>
<span class="ssr-jobno">{j.jobNo}</span>
<span class="ssr-jobmain">
<span class="ssr-jobcust">{j.customer} <em>({j.town})</em></span>
<span class="ssr-jobdesc">{j.desc}</span>
{j.fromMikesCopy && <span class="ssr-recovered">recovered from a hidden tab</span>}
{j.cancelled && <span class="ssr-cancelled">cancelled — kept for the record</span>}
</span>
<span class="ssr-jobside">
<span class={`ssr-status s-${statusOf(j).replace(" ", "")}`}>{statusOf(j)}</span>
<span class={`ssr-jobdate ${focus === "dates" ? "is-focus" : ""}`}>{shortDate(j.start)}</span>
</span>
</button>
{open === j.jobNo && (
<dl class="ssr-detail">
<div><dt>Quoted</dt><dd>{fmtUSD(j.quotedC)}</dd></div>
<div><dt>Invoiced</dt><dd>{j.invoicedC > 0 ? fmtUSD(j.invoicedC) : "—"}</dd></div>
<div><dt>Balance</dt><dd>{fmtUSD(j.quotedC - j.invoicedC)} <small>(computed — it can't dangle)</small></dd></div>
<div><dt>Crew</dt><dd>{j.crew}</dd></div>
<div><dt>Start</dt><dd>{toISO(j.start)}</dd></div>
<div class={focus === "po" ? "is-focus" : ""}><dt>PO #</dt><dd>{j.po ?? "— none recorded"}</dd></div>
{j.notes && <div><dt>Notes</dt><dd>{j.notes}</dd></div>}
<div>
<dt><label for={`st-${j.jobNo}`}>Status</label></dt>
<dd>
<select
id={`st-${j.jobNo}`}
value={statusOf(j)}
onChange={(e) => {
const next = new Map(statusEdits);
next.set(j.jobNo, (e.target as HTMLSelectElement).value as CleanStatus);
setStatusEdits(next);
}}
>
{STATUSES.map((s) => <option value={s}>{s}</option>)}
</select>{" "}
<small>edits live here only — reloads reset this demo</small>
</dd>
</div>
</dl>
)}
</li>
))}
</ul>
<p class={`ssr-total ${focus === "total" ? "is-focus" : ""}`}>
Quoted this year: <strong>{fmtUSD(totalQuotedC)}</strong>{" "}
<small>computed from {active.length} jobs just now — nothing typed in, nothing stale</small>{" "}
<button type="button" class="ssr-whence" aria-expanded={whence} onClick={() => setWhence(!whence)}>
{whence ? "hide the arithmetic" : "where's that from?"}
</button>
</p>
{whence && (
<div class="ssr-whence-panel">
<p>The Quoted column of the list above, added up the moment you opened this:</p>
<table>
<tbody>
{biggest.map((j) => (
<tr><td>#{j.jobNo} {j.customer}</td><td>{fmtUSD(j.quotedC)}</td></tr>
))}
<tr><td>…the other {active.length - biggest.length} jobs</td><td>{fmtUSD(restC)}</td></tr>
<tr class="ssr-whence-sum"><td>= quoted this year</td><td>{fmtUSD(totalQuotedC)}</td></tr>
</tbody>
</table>
<p>
Includes the {recoveredCount} jobs recovered from the hidden tab; leaves out{" "}
{cancelledCount === 1 ? "the 1 cancelled job" : `the ${cancelledCount} cancelled jobs`} kept
for the record. There is no cell where a person could type a different answer — that's the
difference from the old Total row, which had been wrong since spring.
</p>
</div>
)}
</>
) : (
<table class="ssr-sched">
<thead>
<tr><th scope="col">Crew</th>{days.map((d) => <th scope="col">{d}</th>)}</tr>
</thead>
<tbody>
{(["A", "B", "C"] as const).map((crew) => (
<tr>
<th scope="row">{crew}</th>
{days.map((_, day) => {
const slot = data.schedule.find((s) => s.crew === crew && s.day === day);
return <td>{slot ? slot.label : ""}{slot?.confirmed && <span class="ssr-confirmed"> ✓ confirmed</span>}</td>;
})}
</tr>
))}
</tbody>
</table>
)}
</div>
);
}