loopable / web /src /pages /DrillPanel.tsx
fsanyoto's picture
Deploy AIOS web (React glide grid + FastAPI slice)
092334a verified
Raw
History Blame Contribute Delete
3.71 kB
// ---------------------------------------------------------------------------
// pages / DrillPanel.tsx β€” EXIT wave 2 (W2-7, contract Y6).
//
// β›” THIS PANEL IS DELIBERATELY THIN, AND IT SAYS SO ON SCREEN.
//
// Y1 amendment 4: drill descriptors are DECLARATIONS this wave β€” there is no
// resolver route yet. Every descriptor the server sends is complete (window +
// scope + label), so wave 3 adds the resolver with no shape change. Y6 is
// explicit about the consequence: full parity with `ui/drawers.py` (1,473 lines,
// 19 drawer bodies) is NOT in this wave, and the rule is *"say so in the UI
// rather than shipping a panel that looks complete and is not."*
//
// So this panel shows exactly what is true β€” WHICH entity or window you asked
// for, in the words the server used β€” and names what is coming. A panel that
// rendered a plausible-looking empty shell would be worse than this one: behind
// a login, an empty detail view reads as "this customer has no orders".
//
// The owner rule underneath it ([[no-unverifiable-aggregates]]) is why the
// descriptor is echoed in full rather than summarised: the number you clicked
// and the window it covers must be inspectable even before the rows arrive.
// ---------------------------------------------------------------------------
import { fmt } from "../ui";
import type { DecompDescriptor, DrillDescriptor, EntityDescriptor } from "../ui/types";
const ENTITY_WORD: Record<string, string> = {
customer: "Customer",
sku: "Product",
rep: "Salesperson",
bu: "Business Unit",
};
function Row({ label, value }: { label: string; value: string }) {
return (
<div className="pg-drill-row">
<span className="pg-drill-key">{label}</span>
<span className="pg-drill-val">{value}</span>
</div>
);
}
export function DrillPanel({
drill,
onClose,
}: {
drill: DrillDescriptor;
onClose: () => void;
}) {
const isDecomp = drill.kind === "decomp";
const d = drill as DecompDescriptor;
const e = drill as EntityDescriptor;
return (
<aside className="pg-drill" role="dialog" aria-label={drill.label}>
<header className="pg-drill-head">
<div>
<span className="pg-drill-kind">
{isDecomp ? "Period" : (ENTITY_WORD[drill.kind] ?? drill.kind)}
</span>
<h2 className="pg-drill-title">{drill.label}</h2>
</div>
<button type="button" className="pg-drill-close" onClick={onClose} aria-label="Close">
Γ—
</button>
</header>
<div className="pg-drill-body">
{isDecomp ? (
<>
<Row label="Window" value={`${fmt.date(d.date_from)} – ${fmt.date(d.date_to)}`} />
{d.cmp_from && d.cmp_to ? (
<Row label="Compared with" value={`${fmt.date(d.cmp_from)} – ${fmt.date(d.cmp_to)}`} />
) : null}
<Row label="Scope" value={d.scope_value == null ? d.scope_type : `${d.scope_type} Β· ${d.scope_value}`} />
</>
) : (
<>
<Row label={ENTITY_WORD[drill.kind] ?? "Record"} value={e.label} />
<Row label="Identifier" value={String(e.id)} />
</>
)}
{/* The honest sentence. Not a spinner, not "coming soon" in a toast β€”
the limit is part of what this panel is for. */}
<p className="pg-drill-note">
This wave ships the drill as a complete <em>declaration</em>: the exact
scope above is what the number was computed over, and you can check it.
The row-level breakdown behind it opens in the next release β€” until
then, use the same view in the current application.
</p>
</div>
</aside>
);
}