Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Link } from "../../../shared/ui/link";
import {
briefLinkStyle,
neutralActionButtonStyle,
neutralActionButtonToneStyle,
} from "./action-button-styles";
import { StatusIcon } from "./status-dialog";

Expand All @@ -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",
Expand Down Expand Up @@ -62,7 +62,11 @@ export const DocsIconButton = ({
return (
<button
type="button"
className={cx(docsButton, active && docsButtonActive, className)}
className={cx(
docsButton,
active ? docsButtonActive : docsButtonInactive,
className,
)}
aria-label={label}
onClick={onClick}
>
Expand Down Expand Up @@ -104,16 +108,25 @@ export const NeutralActionButton = ({
icon,
children,
className,
tone = "neutral",
}: {
onClick: MouseEventHandler<HTMLButtonElement>;
icon?: ReactNode;
children: ReactNode;
className?: string;
tone?: StatusActionState["tone"];
}) => {
const toneClassName =
tone === "success"
? statusSuccess
: tone === "danger"
? statusDanger
: neutralActionButtonToneStyle;

return (
<button
type="button"
className={cx(neutralActionButtonStyle, className)}
className={cx(neutralActionButtonStyle, toneClassName, className)}
onClick={onClick}
>
{icon}
Expand All @@ -136,11 +149,8 @@ export const StatusActionButton = ({
<NeutralActionButton
onClick={onClick}
icon={<StatusIcon />}
className={cx(
state.tone === "success" && statusSuccess,
state.tone === "danger" && statusDanger,
className,
)}
className={className}
tone={state.tone}
>
{state.label}
</NeutralActionButton>
Expand Down
42 changes: 42 additions & 0 deletions apps/hash-frontend/src/pages/supply-chain/shared/cost.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
computeDailyCost,
computeMonthlyCost,
computePeriodCost,
computePeriodMaterialValue,
costRatePerKgDay,
formatCost,
formatNumber,
Expand Down Expand Up @@ -95,6 +96,47 @@ describe("computePeriodCost", () => {
});
});

describe("computePeriodMaterialValue", () => {
afterEach(() => vi.useRealTimers());

const materialValue = {
unit_cost: 2.5,
currency: "USD",
unit_cost_source: "Database",
uom: "KG",
monthly: [
{ month: "2025-07", quantity: 10 },
{ month: "2026-01", quantity: 20 },
{ month: "2026-02", quantity: 30 },
{ month: "2026-05", quantity: 40 },
{ month: "2026-06", quantity: 50 },
{ month: "2026-07", quantity: 60 },
],
};

it("sums quantities in the selected whole-calendar-month range", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-07-27T12:00:00Z"));

expect(computePeriodMaterialValue(materialValue, "3m")).toBe(
(40 + 50 + 60) * 2.5,
);
expect(computePeriodMaterialValue(materialValue, "6m")).toBe(
(30 + 40 + 50 + 60) * 2.5,
);
expect(computePeriodMaterialValue(materialValue, "12m")).toBe(
(20 + 30 + 40 + 50 + 60) * 2.5,
);
});

it("returns null only when material-value data is unavailable", () => {
expect(computePeriodMaterialValue(null, "3m")).toBeNull();
expect(
computePeriodMaterialValue({ ...materialValue, monthly: [] }, "3m"),
).toBe(0);
});
});

describe("formatCost / formatNumber", () => {
// Pin locale so thousands/decimal separators are deterministic.
afterEach(() => vi.unstubAllGlobals());
Expand Down
25 changes: 25 additions & 0 deletions apps/hash-frontend/src/pages/supply-chain/shared/cost.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { createContext, useContext } from "react";

import { cutoffForRange } from "./time-range";

import type { TimeRange } from "./time-range";
import type { MaterialValueData } from "./types";

export const DEFAULT_WACC = 0.1;
export const DEFAULT_STORAGE_COST = 0.4; // currency units/tonne/day
export const DEFAULT_CURRENCY = "USD";
Expand Down Expand Up @@ -170,6 +175,26 @@ export function computePeriodCost(
);
}

/**
* Standard material value passing through a step in the selected whole-calendar
* month range. This deliberately uses the independent material-flow series,
* rather than timing observations affected by outlier or procurement controls.
*/
export function computePeriodMaterialValue(
materialValue: MaterialValueData | null | undefined,
timeRange: TimeRange,
): number | null {
if (!materialValue) {
return null;
}
const cutoff = cutoffForRange(timeRange);
const quantity = materialValue.monthly.reduce(
(sum, bucket) => (bucket.month >= cutoff ? sum + bucket.quantity : sum),
0,
);
return quantity * materialValue.unit_cost;
}

/** Recompute daily carrying cost from mean quantity and unit price. */
export function computeDailyCost(
meanQty: number | null | undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,20 +95,22 @@ const navEntry = css({
pr: "4",
py: "1",
textStyle: "sm",
color: "fg.subtle",
cursor: "pointer",
bg: "[transparent]",
border: "none",
borderLeftWidth: "2px",
borderLeftStyle: "solid",
borderLeftColor: "[transparent]",
_hover: { color: "fg.muted" },
});
const navEntryActive = css({
color: "fg.heading",
fontWeight: "medium",
borderLeftColor: "fg.heading",
});
const navEntryInactive = css({
color: "fg.subtle",
borderLeftColor: "[transparent]",
});
const content = css({
flex: "1",
minW: "0",
Expand Down Expand Up @@ -334,8 +336,9 @@ export const DocsModal = ({
className={cx(
navEntry,
section.id === activeSection &&
entry.id === activeEntryId &&
navEntryActive,
entry.id === activeEntryId
? navEntryActive
: navEntryInactive,
)}
onClick={() =>
navigate({ section: section.id, sub: entry.id })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@ export const siteOverviewSection: DocSectionDef = {
</CrossRef>{" "}
menu.
</P>
<P>
<Term>Value</Term> is the actual quantity that passed through the
step in the selected period, multiplied by its unit cost, to help in
understanding the scale of impact of miscalibrated parameters.
Depending on the step, quantity means material received, produced,
released from QA or transported. It is not carrying cost or
estimated savings, and outlier controls do not change it.
</P>
</>
),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ export const stepDetailDoc: DocEntry = {
difference from observed timing and the source note where the plan is
not the default planned-delivery-time field.
</LI>
<LI>
<Term>Value</Term>, when available, is the actual quantity received,
produced, released or transported during the selected period
multiplied by the unit cost, to help in understanding the scale of
impact of miscalibrated parameters.
</LI>
</UL>

<H4>Evidence and actions</H4>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,12 @@ const iconButton = 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 iconButtonActive = css({ color: "fg.heading", bg: "bg.subtle" });
const iconButtonInactive = css({ color: "fg.subtle", bg: "bgSolid.min" });
const settingsWrap = css({
mt: "3",
borderTopWidth: "1px",
Expand Down Expand Up @@ -143,7 +142,10 @@ export const HeaderActionButtons = ({
<div className={actionRow}>
<button
type="button"
className={cx(iconButton, settingsOpen && iconButtonActive)}
className={cx(
iconButton,
settingsOpen ? iconButtonActive : iconButtonInactive,
)}
aria-label={
settingsOpen ? "Hide analysis settings" : "Show analysis settings"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ const button = css({
cursor: "pointer",
borderWidth: "1px",
borderStyle: "solid",
borderColor: "[transparent]",
});
const buttonActive = css({
bg: "bgSolid.min",
Expand All @@ -51,6 +50,7 @@ const buttonActive = css({
boxShadow: "sm",
});
const buttonInactive = css({
borderColor: "[transparent]",
color: "fg.subtle",
_hover: { color: "fg.muted" },
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ describe("deduplicateNodes", () => {
plant: "PL-A",
supplier_id: "A",
receipt_basis: "ordinary",
material_value: {
unit_cost: 2,
currency: "USD",
unit_cost_source: "Database",
uom: "KG",
monthly: [{ month: "2026-01", quantity: 100 }],
},
});
const supplierB = node({
...supplierA,
Expand Down Expand Up @@ -110,6 +117,9 @@ describe("deduplicateNodes", () => {
expect(
procurement.find((row) => row.supplier_id === "B")?.products,
).toHaveLength(1);
expect(
procurement.find((row) => row.supplier_id === "A")?.material_value,
).toEqual(supplierA.material_value);
});

it("merges shared raw-material nodes across products by material, plant, type, and series", () => {
Expand Down
Loading
Loading