mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 09:13:11 +00:00
fix(dashboard): map breakdown type keys and open performance drill-downs
The Work Order Breakdown object from the backend serializes its keys without JsonPropertyName, so they arrive lowercase (pm/emergency/reactive/ overdue/other). The mapper copied them straight into `status`, which the card uses as both label and the `types` drill-down id, and `workOrderTypeDrilldownSearch` only accepts canonical WOType values — so every breakdown row rendered lowercase and was inert (SH-354). Route both mapping branches through a normalizer that maps type keys back to their canonical labels case-insensitively; `other` is not a WOType, so it stays a labelled, non-drillable "Other" row, and unknown values (legacy array-shape lifecycle labels) pass through unchanged. Dispatcher Performance gated its drill-down on `completionRate > 0`, so a dispatcher with assigned work but no completions yet was inert here while drillable from Workload. The drill-down filters by dispatcher + range and never uses the rate, so gate on identity instead. Also mock useDashboardTrend in the dashboard page test so it no longer issues a real request that failed quietly in jsdom.
This commit is contained in:
parent
1b17cbec01
commit
c7388ba2d9
4 changed files with 67 additions and 6 deletions
|
|
@ -30,7 +30,13 @@ function DispatcherNameCell({
|
|||
range: DashboardRangeKey;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const drillable = Boolean(row.dispatcherId) && row.completionRate > 0;
|
||||
// A listed dispatcher is one with work in range; the drill-down filters Work
|
||||
// Orders by dispatcher + range and never uses the completion rate, so gate on
|
||||
// identity rather than the rate. (A dispatcher with assigned work but a 0%
|
||||
// rate was previously inert here while drillable from Workload.) When SH-347's
|
||||
// Assigned/Completed counts land on DispatcherPerformanceRow this should
|
||||
// become `assigned > 0`, matching Workload's `totalWorkOrders > 0`.
|
||||
const drillable = Boolean(row.dispatcherId);
|
||||
|
||||
if (!drillable) {
|
||||
return <Text variant="description">{row.dispatcherName}</Text>;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from "@/domain/dashboard/types/dashboard-regions";
|
||||
import type { DispatcherWorkloadRow } from "@/domain/dashboard/types/dashboard-workload";
|
||||
import type { VendorInsightRow } from "@/domain/dashboard/types/dashboard-vendor-insights";
|
||||
import { WO_TYPES } from "@/domain/work-orders/types/work-order-wizard";
|
||||
|
||||
function toNumber(value: unknown): number {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
|
|
@ -100,11 +101,34 @@ function trendSearchParams(params?: DashboardTrendParams): Options | undefined {
|
|||
return { searchParams };
|
||||
}
|
||||
|
||||
const BREAKDOWN_TYPE_BY_LOWER = new Map<string, string>(
|
||||
WO_TYPES.map((type) => [type.toLowerCase(), type]),
|
||||
);
|
||||
|
||||
/**
|
||||
* The backend breakdown object (backend#138) serializes its keys with no
|
||||
* JsonPropertyName, so `PM`/`Emergency`/`Reactive`/`Overdue`/`Other` arrive
|
||||
* lowercase. The card uses the row label as both display text and the
|
||||
* `types` drill-down id, and `workOrderTypeDrilldownSearch` only accepts
|
||||
* canonical `WOType` values — so map lowercase type keys back to their
|
||||
* canonical labels. `other` is not a WOType: keep it as a labelled,
|
||||
* non-drillable "Other" row. Unknown values (e.g. lifecycle labels from the
|
||||
* legacy array shape) pass through unchanged.
|
||||
*/
|
||||
function normalizeBreakdownStatus(raw: string): string {
|
||||
const trimmed = raw.trim();
|
||||
const canonical = BREAKDOWN_TYPE_BY_LOWER.get(trimmed.toLowerCase());
|
||||
if (canonical) return canonical;
|
||||
if (trimmed.toLowerCase() === "other") return "Other";
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function mapBreakdownRow(raw: Record<string, unknown>): DashboardBreakdownRow {
|
||||
const status = asString(
|
||||
readField(raw, "status") ?? readField(raw, "name") ?? readField(raw, "label"),
|
||||
);
|
||||
return {
|
||||
status:
|
||||
asString(readField(raw, "status") ?? readField(raw, "name") ?? readField(raw, "label")) ||
|
||||
UNMAPPED_REGION_LABEL,
|
||||
status: status ? normalizeBreakdownStatus(status) : UNMAPPED_REGION_LABEL,
|
||||
count: toNumber(readField(raw, "count") ?? readField(raw, "value")),
|
||||
};
|
||||
}
|
||||
|
|
@ -116,7 +140,10 @@ function mapBreakdown(value: unknown): DashboardBreakdownRow[] {
|
|||
|
||||
if (typeof value === "object" && value !== null) {
|
||||
return Object.entries(value as Record<string, unknown>)
|
||||
.map(([status, count]) => ({ status, count: toNumber(count) }))
|
||||
.map(([status, count]) => ({
|
||||
status: normalizeBreakdownStatus(status),
|
||||
count: toNumber(count),
|
||||
}))
|
||||
.filter((row) => row.status !== "");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,15 @@ vi.mock("@/domain/dashboard/use-cases/use-dashboard-vendor-insights", () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/domain/dashboard/use-cases/use-dashboard-trend", () => ({
|
||||
useDashboardTrend: () => ({
|
||||
data: { granularity: null, points: [] },
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
error: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
const statsData = {
|
||||
scheduledTomorrow: 3,
|
||||
pendingUplifts: 5,
|
||||
|
|
@ -139,7 +148,8 @@ describe("DashboardPage", () => {
|
|||
it("shows the documented empty states when datasets are empty", () => {
|
||||
renderDashboard();
|
||||
|
||||
expect(screen.getByText("No work orders match the current filters.")).toBeInTheDocument();
|
||||
// Breakdown and Trend share this empty copy, so both cards surface it.
|
||||
expect(screen.getAllByText("No work orders match the current filters.")).toHaveLength(2);
|
||||
expect(screen.getAllByText("No dispatchers are set up yet.")).toHaveLength(2);
|
||||
expect(screen.getByText("No active vendor has any work orders yet.")).toBeInTheDocument();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -58,6 +58,24 @@ describe("dashboardApi", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("maps the object breakdown shape to canonical WOType labels", async () => {
|
||||
apiGet.mockResolvedValue({
|
||||
Data: {
|
||||
Breakdown: { pm: 3, emergency: 1, reactive: "2", overdue: 4, other: 5 },
|
||||
},
|
||||
});
|
||||
|
||||
const stats = await dashboardApi.getStats();
|
||||
|
||||
expect(stats.breakdown).toEqual([
|
||||
{ status: "PM", count: 3 },
|
||||
{ status: "Emergency", count: 1 },
|
||||
{ status: "Reactive", count: 2 },
|
||||
{ status: "Overdue", count: 4 },
|
||||
{ status: "Other", count: 5 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("omits search params for the all-time range", async () => {
|
||||
apiGet.mockResolvedValue({ scheduledTomorrow: 0, pendingUplifts: 0, avetaPending: 0 });
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue