diff --git a/apps/hash-api/src/seed-data/supply-chain-dataset-import.test.ts b/apps/hash-api/src/seed-data/supply-chain-dataset-import.test.ts index d9130f8b533..eb662cb7c93 100644 --- a/apps/hash-api/src/seed-data/supply-chain-dataset-import.test.ts +++ b/apps/hash-api/src/seed-data/supply-chain-dataset-import.test.ts @@ -51,7 +51,7 @@ describe("supply-chain dataset import", () => { writeJson( path.join(sourceDir, "demo-product", "production_schedule.json"), { - schema_version: "1.1", + schema_version: scheduleVersion === "1.2" ? "1.2" : "1.1", artifact_type: "production_schedule", artifact_version: scheduleVersion, product_id: "demo-product", diff --git a/apps/hash-frontend/src/pages/supply-chain/shared/action-button-styles.ts b/apps/hash-frontend/src/pages/supply-chain/shared/action-button-styles.ts index 21d88385cb3..e740947d73c 100644 --- a/apps/hash-frontend/src/pages/supply-chain/shared/action-button-styles.ts +++ b/apps/hash-frontend/src/pages/supply-chain/shared/action-button-styles.ts @@ -28,15 +28,18 @@ export const neutralActionButtonStyle = css({ borderRadius: "sm", borderWidth: "1px", borderStyle: "solid", - borderColor: "bd.subtle", - bg: "bgSolid.min", px: "2.5", py: "1", textStyle: "xs", lineHeight: "none", fontWeight: "medium", - color: "fg.muted", cursor: "pointer", whiteSpace: "nowrap", +}); + +export const neutralActionButtonToneStyle = css({ + borderColor: "bd.subtle", + bg: "bgSolid.min", + color: "fg.muted", _hover: { borderColor: "bd.strong", color: "fg.heading" }, }); diff --git a/apps/hash-frontend/src/pages/supply-chain/shared/action-buttons.tsx b/apps/hash-frontend/src/pages/supply-chain/shared/action-buttons.tsx index b9f7c46fba4..ac12520d56b 100644 --- a/apps/hash-frontend/src/pages/supply-chain/shared/action-buttons.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/shared/action-buttons.tsx @@ -5,6 +5,7 @@ import { Link } from "../../../shared/ui/link"; import { briefLinkStyle, neutralActionButtonStyle, + neutralActionButtonToneStyle, } from "./action-button-styles"; import { StatusIcon } from "./status-dialog"; @@ -22,13 +23,12 @@ const docsButton = css({ borderWidth: "1px", borderStyle: "solid", borderColor: "bd.subtle", - bg: "bgSolid.min", - color: "fg.subtle", cursor: "pointer", transition: "colors", _hover: { color: "fg.heading", bg: "bg.subtle" }, }); const docsButtonActive = css({ color: "fg.heading", bg: "bg.subtle" }); +const docsButtonInactive = css({ color: "fg.subtle", bg: "bgSolid.min" }); const statusSuccess = css({ borderColor: "status.success.bd.subtle", bg: "status.success.bg.subtle", @@ -62,7 +62,11 @@ export const DocsIconButton = ({ return ( + )} + + , + ); + + const dialog = screen.getByRole("dialog", { name: "Step details" }); + const positioner = dialog.parentElement as HTMLElement; + + act(() => { + const queuedFrames = animationFrames.splice(0); + for (const animationFrame of queuedFrames) { + animationFrame(0); + } + }); + expect(dialog.style.right).toBe("0px"); + + fireEvent.click(screen.getByRole("button", { name: "Close" })); + + expect(positioner.style.inset).toBe("0"); + expect(positioner.style.right).toBe(""); + expect(dialog.style.right).toBe("-960px"); + expect(dialog.style.transition).toBe("right 200ms ease-out"); + }); +}); diff --git a/apps/hash-frontend/src/pages/supply-chain/shared/slide-over.tsx b/apps/hash-frontend/src/pages/supply-chain/shared/slide-over.tsx index cdb3feff6d0..15ad6c10a55 100644 --- a/apps/hash-frontend/src/pages/supply-chain/shared/slide-over.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/shared/slide-over.tsx @@ -18,12 +18,13 @@ import { css, cx } from "@hashintel/ds-helpers/css"; * click. Portals into the supply-chain layout scope so tokens resolve. Caller * owns header + body. * - * Enter/exit animation: the panel slides via the positioner's `right` offset and - * the backdrop fades, driven by a local `visible` flag. We deliberately animate - * `right` (not `transform`) so no containing block is created — the data-table - * modal renders inline inside the content with `position: fixed` and must stay - * relative to the viewport. On close we play the exit animation, then defer the - * caller's `onClose` (which unmounts us) until the transition finishes. + * Enter/exit animation: the panel slides via its `right` offset while the + * full-viewport positioner stays fixed and the backdrop fades, driven by a + * local `visible` flag. We deliberately animate `right` (not `transform`) so no + * containing block is created — the data-table modal renders inline inside the + * content with `position: fixed` and must stay relative to the viewport. On + * close we play the exit animation, then defer the caller's `onClose` (which + * unmounts us) until the transition finishes. */ const EXIT_MS = 200; @@ -65,21 +66,14 @@ const backdropBase = css({ inset: "0", bg: "neutral.a80", zIndex: "overlay", - opacity: "[0]", transition: "[opacity 200ms ease-out]", }); -const backdropVisible = css({ opacity: "[1]" }); const positionerBase = css({ position: "fixed", - top: "0", - bottom: "0", - right: "[-960px]", display: "flex", zIndex: "modal", - transition: "[right 200ms ease-out]", }); -const positionerVisible = css({ right: "0" }); const contentStyles = css({ height: "full", @@ -123,14 +117,21 @@ export const SlideOver = ({ > {children} diff --git a/apps/hash-frontend/src/pages/supply-chain/shared/step-detail-panel.tsx b/apps/hash-frontend/src/pages/supply-chain/shared/step-detail-panel.tsx index fb20832729d..95c21efc208 100644 --- a/apps/hash-frontend/src/pages/supply-chain/shared/step-detail-panel.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/shared/step-detail-panel.tsx @@ -266,7 +266,6 @@ const segButton = css({ cursor: "pointer", borderWidth: "1px", borderStyle: "solid", - borderColor: "[transparent]", }); const segButtonActive = css({ bg: "bgSolid.min", @@ -275,6 +274,7 @@ const segButtonActive = css({ boxShadow: "sm", }); const segButtonInactive = css({ + borderColor: "[transparent]", color: "fg.subtle", _hover: { color: "fg.muted" }, }); diff --git a/apps/hash-frontend/src/pages/supply-chain/shared/step-detail-panel/timing-metrics.tsx b/apps/hash-frontend/src/pages/supply-chain/shared/step-detail-panel/timing-metrics.tsx index e972b9379a7..703ed6963f0 100644 --- a/apps/hash-frontend/src/pages/supply-chain/shared/step-detail-panel/timing-metrics.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/shared/step-detail-panel/timing-metrics.tsx @@ -5,6 +5,7 @@ import { css, cx } from "@hashintel/ds-helpers/css"; import { isDwellType } from "../categories"; import { useCostParams, + computePeriodMaterialValue, computePeriodCost, formatCost, formatNumber, @@ -45,6 +46,13 @@ const cellCenter = css({ flexDirection: "column", }); const cellPlain = css({ flex: "1", p: "3" }); +const cellSeparated = css({ + flex: "1", + p: "3", + borderLeftWidth: "1px", + borderLeftStyle: "solid", + borderLeftColor: "bd.subtle", +}); const label = css({ textStyle: "xs", fontWeight: "medium", @@ -74,11 +82,6 @@ const valueBase = css({ }); const valueDanger = css({ color: "status.error.fg.body" }); const valueStrong = css({ color: "fg.max" }); -const valueMuted = css({ - textStyle: "base", - fontWeight: "medium", - color: "fg.subtle", -}); const badge = css({ textStyle: "xs", fontWeight: "medium", @@ -105,10 +108,13 @@ const badgeNeutral = css({ borderColor: "bd.subtle", borderRadius: "sm", px: "1.5", - h: "4", + py: "0.5", + minH: "4", lineHeight: "[11px]", display: "inline-flex", alignItems: "center", + whiteSpace: "nowrap", + flexShrink: "0", }); const policyCard = css({ borderWidth: "1px", @@ -219,7 +225,7 @@ export const KeyMetricsRow = ({ // Secondary procurement lead-time figure. `complete_timing` carries the other // basis after load-time row derivation (full-receipt when the headline is // first, and vice-versa once the basis swap runs), so this cell shows the - // counterpart and its gap vs the headline. + // counterpart and its gap from the currently selected receipt basis. const secondaryStats = step.complete_timing?.stats; const secondaryValue = secondaryStats ? selectStat(secondaryStats, effectiveMeasure) @@ -231,6 +237,8 @@ export const KeyMetricsRow = ({ : null; const secondaryLabel = basis === "complete" ? "First receipt" : "Full receipt"; + const headlineBasisLabel = + basis === "complete" ? "full receipt" : "first receipt"; const showCost = isDwellType(step.type) && step.cost != null; const costComparison = useMemo(() => { @@ -263,6 +271,22 @@ export const KeyMetricsRow = ({ storageCost, ) : null; + const materialValue = unfilteredStep.material_value; + const periodMaterialValue = computePeriodMaterialValue( + materialValue, + timeRange, + ); + const materialValueTitle = materialValue + ? [ + `Unit cost: ${formatCost(materialValue.unit_cost, materialValue.currency)}`, + materialValue.uom ? `per ${materialValue.uom}` : null, + materialValue.unit_cost_source + ? `source: ${materialValue.unit_cost_source}` + : null, + ] + .filter(Boolean) + .join("; ") + : undefined; const planNote = step.plan_note; const planLabel = planNote ? planSourceLabel(planNote) : null; @@ -303,26 +327,6 @@ export const KeyMetricsRow = ({ -
-
Median vs previous period
-
- {comparison?.medianPctChange != null ? ( - - ) : ( - - )} -
-
- {secondaryValue != null && (
@@ -334,7 +338,9 @@ export const KeyMetricsRow = ({ {secondaryGap != null && Math.abs(secondaryGap) >= 0.5 && ( - {`${secondaryGap > 0 ? "+" : "\u2212"}${formatNumber(Math.abs(secondaryGap), { maximumFractionDigits: 0 })}d vs headline`} + {`${formatNumber(Math.abs(secondaryGap), { maximumFractionDigits: 0 })}d ${ + secondaryGap > 0 ? "later" : "earlier" + } than ${headlineBasisLabel}`} )}
@@ -374,6 +380,19 @@ export const KeyMetricsRow = ({
)} + + {materialValue && ( +
+
Value
+
+ + {formatCost(periodMaterialValue, materialValue.currency, { + compact: true, + })} + +
+
+ )} ); diff --git a/apps/hash-frontend/src/pages/supply-chain/shared/supplier-otif.test.ts b/apps/hash-frontend/src/pages/supply-chain/shared/supplier-otif.test.ts index bcf3db86b64..9a991301fbf 100644 --- a/apps/hash-frontend/src/pages/supply-chain/shared/supplier-otif.test.ts +++ b/apps/hash-frontend/src/pages/supply-chain/shared/supplier-otif.test.ts @@ -162,7 +162,7 @@ describe("supplier line materialisation", () => { it("materialises site vendor monthly, worst events, and materials from raw lines", () => { const perf: SiteSupplierPerformance = { - schema_version: "1.1", + schema_version: "1.2", generated_at: "2026-06-01T00:00:00Z", overall: { n_lines: 3, diff --git a/apps/hash-frontend/src/pages/supply-chain/shared/types.ts b/apps/hash-frontend/src/pages/supply-chain/shared/types.ts index ec40363e83f..6ee00ac12a5 100644 --- a/apps/hash-frontend/src/pages/supply-chain/shared/types.ts +++ b/apps/hash-frontend/src/pages/supply-chain/shared/types.ts @@ -45,6 +45,17 @@ export interface CostData { unit_price_source?: string | null; } +export interface MaterialValueData { + unit_cost: number; + currency: string; + unit_cost_source: string | null; + uom: string | null; + monthly: Array<{ + month: string; + quantity: number; + }>; +} + /** * Source provenance for a step's durations. Attached by the corresponding * `extract_*` function on the backend so the UI can show users where the @@ -188,6 +199,7 @@ export interface GraphNode { /** Client-derived from the active timing series and `plan`; omitted on the wire. */ pct_exceeding_plan?: number | null; cost: CostData | null; + material_value?: MaterialValueData | null; observations?: Observation[]; /** Client-side cache of combined procurement node observations from the wire. */ procurement_observations?: ProcurementNodeObservation[]; @@ -397,7 +409,7 @@ export interface BindingScore { export interface GraphData { /** Procurement planning data contract version. */ - schema_version?: "1.1"; + schema_version?: "1.2"; analysis_settings?: AnalysisSettings | null; product_id: string; product_name: string; @@ -596,7 +608,7 @@ export interface SiteData { export interface StepDetail { /** Procurement planning data contract version. */ - schema_version?: "1.1"; + schema_version?: "1.2"; id: string; label: string; type: StepType; @@ -613,6 +625,7 @@ export interface StepDetail { /** Client-derived from the active timing series and `plan`; omitted on the wire. */ pct_exceeding_plan?: number | null; cost: CostData | null; + material_value?: MaterialValueData | null; detail_rows?: DetailRows | null; ref_date_col?: string | null; /** @@ -776,7 +789,7 @@ export interface ProcurementSupplierBlock { */ export interface SiteSupplierPerformance { /** Procurement planning data contract version. */ - schema_version?: "1.1"; + schema_version?: "1.2"; generated_at: string; overall: { n_lines: number; @@ -830,7 +843,7 @@ export interface SiteSummaryRollups { /** `site/{siteId}/summary.json` — the precomputed site overview artifact. */ export interface SiteSummary { - schema_version?: "1.1"; + schema_version?: "1.2"; analysis_settings?: AnalysisSettings | null; site_id: string; generated_at: string; diff --git a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/product/e2e-what-if/what-if-kpis.tsx b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/product/e2e-what-if/what-if-kpis.tsx index dc2b6b5a1ec..1eb1254cdd5 100644 --- a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/product/e2e-what-if/what-if-kpis.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/product/e2e-what-if/what-if-kpis.tsx @@ -5,13 +5,13 @@ import { formatNumber } from "../../../shared/cost"; import type { Caveat } from "./caveats"; const tile = css({ - bg: "bg.subtle", borderRadius: "lg", p: "2.5", display: "flex", flexDirection: "column", gap: "1.5", }); +const tileDefault = css({ bg: "bg.subtle" }); const tileSuccess = css({ bg: "status.success.bg.subtle", borderWidth: "1px", @@ -84,7 +84,7 @@ export const KpiTile = ({ : null; const deltaClass = delta != null && delta < 0 ? deltaGood : deltaMuted; return ( -
+
{label}
{value}
diff --git a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/product/production-schedule.test.tsx b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/product/production-schedule.test.tsx index 5f60fa680af..28b3f899a04 100644 --- a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/product/production-schedule.test.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/product/production-schedule.test.tsx @@ -61,7 +61,7 @@ const makeBatch = ( }); const schedule: ProductionScheduleV12 = { - schema_version: "1.1", + schema_version: "1.2", artifact_type: "production_schedule", artifact_version: "1.2", product_id: "product", diff --git a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/product/production-schedule/model.test.ts b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/product/production-schedule/model.test.ts index caba66b0730..69b359db896 100644 --- a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/product/production-schedule/model.test.ts +++ b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/product/production-schedule/model.test.ts @@ -203,7 +203,7 @@ const v12Schedule = ({ links?: ProductionScheduleV12["batch_links"]; dispatchEvents?: ProductionScheduleV12["dispatch_events"]; }): ProductionScheduleV12 => ({ - schema_version: schedule.schema_version, + schema_version: "1.2", artifact_type: schedule.artifact_type, artifact_version: "1.2", product_id: schedule.product_id, diff --git a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/product/production-schedule/schedule-summary.tsx b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/product/production-schedule/schedule-summary.tsx index 446150e1b58..a85c1daa114 100644 --- a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/product/production-schedule/schedule-summary.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/product/production-schedule/schedule-summary.tsx @@ -1,4 +1,5 @@ import { css, cx } from "@hashintel/ds-helpers/css"; +import { token } from "@hashintel/ds-helpers/tokens"; const INVENTORY_DWELL_COLOR = "#f3f8ff"; const USED_ELSEWHERE_HATCH = @@ -131,15 +132,12 @@ export const ProductionScheduleSummary = ({ {hasOverDepletedInventory && ( Over-depleted inventory diff --git a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site.tsx b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site.tsx index 658e94e1da7..40a13f20079 100644 --- a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site.tsx @@ -196,11 +196,10 @@ const SiteSearchInput = ({ const settingsCollapse = css({ display: "grid", - gridTemplateRows: "0fr", - opacity: "0", transition: "[grid-template-rows 180ms ease, opacity 160ms ease]", overflow: "hidden", }); +const settingsCollapseClosed = css({ gridTemplateRows: "0fr", opacity: "0" }); const settingsCollapseOpen = css({ gridTemplateRows: "1fr", opacity: "1" }); const settingsCollapseInner = css({ minH: "0", overflow: "hidden" }); @@ -589,7 +588,10 @@ export const SiteOverview = ({
diff --git a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/opportunities.test.ts b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/opportunities.test.ts index 5544533e0a7..64b326844eb 100644 --- a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/opportunities.test.ts +++ b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/opportunities.test.ts @@ -66,6 +66,7 @@ function planning(overrides: Partial): PlanningRow { pct_exceeding_plan: 30, cost: null, products: [{ id: "p1", name: "Product 1" }], + periodMaterialValue: null, deviationPct: 20, trendPct: null, previousValue: null, diff --git a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/planning-table.tsx b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/planning-table.tsx index e2e119d8fae..2a72eeed117 100644 --- a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/planning-table.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/planning-table.tsx @@ -4,7 +4,7 @@ import { cx } from "@hashintel/ds-helpers/css"; import { StatusActionButton } from "../../shared/action-buttons"; import { getCategoryColor } from "../../shared/categories"; -import { formatNumber } from "../../shared/cost"; +import { formatCost, formatNumber } from "../../shared/cost"; import { MEASURE_LABELS, selectStat, @@ -239,6 +239,16 @@ export const PlanningTable = ({ + + toggleSort("materialValue"), + }} + /> + + + {formatCost( + row.periodMaterialValue, + row.material_value?.currency ?? null, + { compact: true }, + )} + {formatNumber(row.plan, { maximumFractionDigits: 0 })}d @@ -428,7 +445,7 @@ export const PlanningTable = ({ })} {displayedRows.length === 0 && ( - + {rows.length === 0 ? "No planning parameter data for this site." : "No planning parameter data matches the current filters."} diff --git a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/shared/filter-menu.tsx b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/shared/filter-menu.tsx index 4375c425fa2..1ce9e40563f 100644 --- a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/shared/filter-menu.tsx +++ b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/shared/filter-menu.tsx @@ -29,12 +29,12 @@ const triggerButton = css({ justifyContent: "center", p: "0.5", borderRadius: "sm", - color: "fg.subtle", cursor: "pointer", transition: "colors", _hover: { bg: "bg.subtle" }, }); const triggerActive = css({ color: "[#2563eb]" }); +const triggerInactive = css({ color: "fg.subtle" }); const content = css({ display: "flex", @@ -206,7 +206,10 @@ export const FilterMenu = ({ diff --git a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/shared/helpers.test.ts b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/shared/helpers.test.ts new file mode 100644 index 00000000000..abe90005a43 --- /dev/null +++ b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/shared/helpers.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; + +import { sortPlanningRows } from "./helpers"; + +import type { PlanningRow } from "./row-types"; + +const planningRow = ( + label: string, + periodMaterialValue: number | null, +): PlanningRow => + ({ + label, + periodMaterialValue, + }) as unknown as PlanningRow; + +describe("sortPlanningRows material value", () => { + const rows = [ + planningRow("missing", null), + planningRow("high", 500), + planningRow("low", 100), + ]; + + it("sorts ascending with missing values last", () => { + expect( + sortPlanningRows(rows, { key: "materialValue", dir: "asc" }).map( + (row) => row.label, + ), + ).toEqual(["low", "high", "missing"]); + }); + + it("sorts descending with missing values last", () => { + expect( + sortPlanningRows(rows, { key: "materialValue", dir: "desc" }).map( + (row) => row.label, + ), + ).toEqual(["high", "low", "missing"]); + }); +}); diff --git a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/shared/helpers.ts b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/shared/helpers.ts index b1d95a3edbc..74337cab19e 100644 --- a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/shared/helpers.ts +++ b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/shared/helpers.ts @@ -165,6 +165,13 @@ export function sortPlanningRows( : Number.POSITIVE_INFINITY; va = left.deviationPct ?? nullRank; vb = right.deviationPct ?? nullRank; + } else if (sort.key === "materialValue") { + const nullRank = + sort.dir === "desc" + ? Number.NEGATIVE_INFINITY + : Number.POSITIVE_INFINITY; + va = left.periodMaterialValue ?? nullRank; + vb = right.periodMaterialValue ?? nullRank; } else if (sort.key === "exceeding") { va = left.pct_exceeding_plan ?? 0; vb = right.pct_exceeding_plan ?? 0; diff --git a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/shared/row-types.ts b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/shared/row-types.ts index f619c488984..3789d1b7f05 100644 --- a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/shared/row-types.ts +++ b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/shared/row-types.ts @@ -7,6 +7,7 @@ export type Tab = "dwell" | "planning" | "trends" | "suppliers"; export type SortKey = | "median" | "cost" + | "materialValue" | "moq" | "safetyStock" | "material" @@ -46,6 +47,7 @@ export type DwellRow = SiteNode & { }; export type PlanningRow = SiteNode & { + periodMaterialValue: number | null; /** Null when the applicable plan is zero: the row remains visible but no percentage is meaningful. */ deviationPct: number | null; trendPct: number | null; diff --git a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/use-site-overview-rows.ts b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/use-site-overview-rows.ts index 059d50b5b6c..2861a764a4d 100644 --- a/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/use-site-overview-rows.ts +++ b/apps/hash-frontend/src/pages/supply-chain/supply-chain-data-shell/site/use-site-overview-rows.ts @@ -1,7 +1,11 @@ import { useEffect, useMemo, useState } from "react"; import { DWELL_TYPES } from "../../shared/categories"; -import { useCostParams, useOutlierSetting } from "../../shared/cost"; +import { + computePeriodMaterialValue, + useCostParams, + useOutlierSetting, +} from "../../shared/cost"; import { fetchSupplierPerformance } from "../../shared/data"; import { selectStat, useBaseMeasure } from "../../shared/measure-context"; import { applyOutlierSelectionToNode } from "../../shared/outlier-selection"; @@ -130,6 +134,10 @@ export function useSiteOverviewRows({ return new Map(historicalNodes.map((count) => [siteNodeKey(count), count])); }, [historicalNodes]); + const sourceNodesByKey = useMemo(() => { + return new Map(dedupedNodes.map((count) => [siteNodeKey(count), count])); + }, [dedupedNodes]); + const planningVisibleNodes = useMemo(() => { if (!excludeLowSamples) { return filteredNodes; @@ -222,6 +230,7 @@ export function useSiteOverviewRows({ return []; } const historical = historicalNodesByKey.get(siteNodeKey(count)) ?? count; + const source = sourceNodesByKey.get(siteNodeKey(count)) ?? count; const trend = computeTimingTrend(historical, timeRange, measure); const deviationPct = plan <= 0 @@ -237,13 +246,23 @@ export function useSiteOverviewRows({ return { ...count, plan, + periodMaterialValue: computePeriodMaterialValue( + source.material_value, + timeRange, + ), deviationPct, trendPct: trend.pctChange, previousValue: trend.previousValue, previousTrendN: trend.previousN, }; }); - }, [planningVisibleNodes, historicalNodesByKey, timeRange, measure]); + }, [ + planningVisibleNodes, + historicalNodesByKey, + sourceNodesByKey, + timeRange, + measure, + ]); const windowedSupplier = useMemo(() => { if (!supplierData) { diff --git a/libs/@local/hash-isomorphic-utils/src/production-schedule/schema.ts b/libs/@local/hash-isomorphic-utils/src/production-schedule/schema.ts index c437159db17..091389d47af 100644 --- a/libs/@local/hash-isomorphic-utils/src/production-schedule/schema.ts +++ b/libs/@local/hash-isomorphic-utils/src/production-schedule/schema.ts @@ -254,7 +254,6 @@ export const productionScheduleDispatchEventSchema = z.strictObject({ }); const scheduleBaseShape = { - schema_version: z.literal("1.1"), artifact_type: z.literal("production_schedule"), product_id: nonEmptyString, product_name: nonEmptyString, @@ -273,6 +272,7 @@ const sourceBaseShape = { export const productionScheduleLegacySchema = z.strictObject({ ...scheduleBaseShape, + schema_version: z.literal("1.1"), artifact_version: z.union([z.literal("1.0"), z.literal("1.1")]), lanes: z.array(productionScheduleLegacyLaneSchema).min(1), consumption_evidence: z.array(productionScheduleConsumptionEvidenceSchema), @@ -285,6 +285,7 @@ export const productionScheduleLegacySchema = z.strictObject({ export const productionScheduleV12Schema = z.strictObject({ ...scheduleBaseShape, + schema_version: z.literal("1.2"), artifact_version: z.literal("1.2"), lanes: z.array(productionScheduleV12LaneSchema).min(1), consumption_events: z.array(productionScheduleConsumptionEventSchema),