mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* [d700055f] feat(scorecard): replace StubObjectivesSection with live charter objective cards Delete the StubObjectivesSection placeholder (fake 'Revenue growth'/'Customer retention' labels) and render a real ObjectivesSection with three positional charter objective cards, each showing its live metric against its target: first_pass_yield (90%), median_lead_time_hours (<24h), escaped_defects (0). - CockpitSummary: add optional first_pass_yield and escaped_defects fields (backend companion item not yet shipped; UI renders 'No data yet' until then) - ObjectivesSection: positional mapping objectives[i].metric -> metric i, documented as a positional-by-convention assumption; canonical fallback labels when the charter objectives array is empty/shorter than 3; 'No data yet' italic muted fallback for null/undefined metrics (SpeedSection pattern); DeliveryMetric card styling; first_pass_yield formatted as pctOrDash does - SpeedSection kept as-is; ObjectivesSection references the same median_lead_time_hours value as a peer card alongside the other two - Tests: buildSummary carries the new fields; cover present/missing metrics and absence of the fake stub labels; existing lead-time/null tests adjusted for the now-shared value and multi-card 'No data yet' * [d700055f] docs(scorecard): document live ObjectivesSection and new CockpitSummary fields Add panel/docs/frontend/company-scorecard-card.md covering the new ObjectivesSection: the three positional charter objective cards (first_pass_yield 90%, median_lead_time_hours <24h, escaped_defects 0), the positional-by-convention mapping, canonical fallback labels, the 'No data yet' fallback pattern, and the two new optional CockpitSummary fields with the backend companion-item caveat. Add a Key Symbols row for CompanyScorecardCard/ObjectivesSection in docs/map/panel.md. --------- Co-authored-by: roboco-app[bot] <302741806+roboco-app[bot]@users.noreply.github.com> Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> Co-authored-by: Frontend PM <fe-pm@roboco.tech>
This commit is contained in:
co-authored by
roboco-app[bot] <302741806+roboco-app[bot]@users.noreply.github.com>
Frontend Developer 2
Frontend Documenter
Frontend PM
parent
80ccf415cb
commit
b3dda00e41
@@ -239,32 +239,112 @@ function SpeedSection({ medianLeadTimeHours }: SpeedSectionProps) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stub objectives section
|
||||
// Objectives section — three charter objective cards, each showing a live
|
||||
// metric against its target.
|
||||
//
|
||||
// ponytail: The charter `objectives` field is free text
|
||||
// (Record<string, unknown>[]) while the three metrics are hardcoded
|
||||
// (first_pass_yield, median_lead_time_hours, escaped_defects). The mapping
|
||||
// is positional-by-convention, not derived — one card per charter objective
|
||||
// only holds until you edit the Goals tab. Stated assumption, not
|
||||
// discovered later.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function StubObjectivesSection() {
|
||||
const stubs = [
|
||||
{ id: "obj-1", label: "Revenue growth" },
|
||||
{ id: "obj-2", label: "Customer retention" },
|
||||
];
|
||||
const OBJECTIVE_FALLBACK_LABELS = [
|
||||
"Tasks shipped to merge with no human code edits",
|
||||
"Median lead time, intake → merged",
|
||||
"Critical escaped defects per release",
|
||||
] as const;
|
||||
|
||||
interface ObjectivesSectionProps {
|
||||
objectives: Record<string, unknown>[];
|
||||
firstPassYield: number | null | undefined;
|
||||
medianLeadTimeHours: number | null | undefined;
|
||||
escapedDefects: number | null | undefined;
|
||||
}
|
||||
|
||||
function objectiveLabel(
|
||||
objectives: Record<string, unknown>[],
|
||||
index: number,
|
||||
): string {
|
||||
const raw = objectives[index]?.metric;
|
||||
return typeof raw === "string" && raw.length > 0
|
||||
? raw
|
||||
: OBJECTIVE_FALLBACK_LABELS[index];
|
||||
}
|
||||
|
||||
interface ObjectiveCardProps {
|
||||
label: string;
|
||||
hasData: boolean;
|
||||
formattedValue: string;
|
||||
targetText: string;
|
||||
}
|
||||
|
||||
function ObjectiveCard({
|
||||
label,
|
||||
hasData,
|
||||
formattedValue,
|
||||
targetText,
|
||||
}: ObjectiveCardProps) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-3 space-y-1">
|
||||
<div className="text-xs text-muted-foreground">{label}</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
{hasData ? (
|
||||
<span className="font-medium tabular-nums">{formattedValue}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground italic">No data yet</span>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
target: {targetText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ObjectivesSection({
|
||||
objectives,
|
||||
firstPassYield,
|
||||
medianLeadTimeHours,
|
||||
escapedDefects,
|
||||
}: ObjectivesSectionProps) {
|
||||
// Show 'No data yet' when a metric is null or undefined — never a fabricated
|
||||
// value. Mirrors SpeedSection's hasData guard.
|
||||
const fpyHasData = firstPassYield != null;
|
||||
const ltHasData = medianLeadTimeHours != null;
|
||||
const edHasData = escapedDefects != null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<HelpTip label="Placeholder — not yet wired to the Goals tab's Objectives list">
|
||||
<SectionLabel>Objectives</SectionLabel>
|
||||
</HelpTip>
|
||||
<SectionLabel>Objectives</SectionLabel>
|
||||
<div className="space-y-2">
|
||||
{stubs.map((stub) => (
|
||||
<div
|
||||
key={stub.id}
|
||||
className="rounded-lg border border-dashed p-3 flex items-center justify-between"
|
||||
>
|
||||
<span className="text-sm text-muted-foreground">{stub.label}</span>
|
||||
<span className="text-xs text-muted-foreground italic">
|
||||
Not tracked yet
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<ObjectiveCard
|
||||
label={objectiveLabel(objectives, 0)}
|
||||
hasData={fpyHasData}
|
||||
formattedValue={
|
||||
fpyHasData
|
||||
? `${((firstPassYield as number) * 100).toFixed(0)}%`
|
||||
: ""
|
||||
}
|
||||
targetText="90%"
|
||||
/>
|
||||
<ObjectiveCard
|
||||
label={objectiveLabel(objectives, 1)}
|
||||
hasData={ltHasData}
|
||||
formattedValue={
|
||||
ltHasData
|
||||
? `${(medianLeadTimeHours as number).toFixed(1)}h`
|
||||
: ""
|
||||
}
|
||||
targetText="< 24h"
|
||||
/>
|
||||
<ObjectiveCard
|
||||
label={objectiveLabel(objectives, 2)}
|
||||
hasData={edHasData}
|
||||
formattedValue={edHasData ? `${escapedDefects as number}` : ""}
|
||||
targetText="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -299,7 +379,12 @@ function ScorecardBody({
|
||||
spendTrendLoading={spendTrendLoading}
|
||||
/>
|
||||
<SpeedSection medianLeadTimeHours={data.median_lead_time_hours} />
|
||||
<StubObjectivesSection />
|
||||
<ObjectivesSection
|
||||
objectives={data.objectives}
|
||||
firstPassYield={data.first_pass_yield}
|
||||
medianLeadTimeHours={data.median_lead_time_hours}
|
||||
escapedDefects={data.escaped_defects}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user