feat: add impact delta computation with TDD tests

Implements computeImpactDeltas pure function with 8 TDD tests covering replace/add/none modes and null weight/price handling. Adds useImpactDeltas hook, categoryId to ThreadWithCandidates, and selectedSetupId state to uiStore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 18:06:46 +02:00
parent 1a5e6a303e
commit 818db73432
6 changed files with 213 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
export interface CandidateInput {
id: number;
weightGrams: number | null;
priceCents: number | null;
}
export interface SetupItemInput {
categoryId: number;
weightGrams: number | null;
priceCents: number | null;
name: string;
}
export type DeltaMode = "replace" | "add" | "none";
export interface CandidateDelta {
candidateId: number;
mode: DeltaMode;
weightDelta: number | null;
priceDelta: number | null;
replacedItemName: string | null;
}
export interface ImpactDeltas {
mode: DeltaMode;
deltas: Record<number, CandidateDelta>;
}
export function computeImpactDeltas(
candidates: CandidateInput[],
setupItems: SetupItemInput[] | undefined,
threadCategoryId: number,
): ImpactDeltas {
if (!setupItems) return { mode: "none", deltas: {} };
const replacedItem =
setupItems.find((item) => item.categoryId === threadCategoryId) ?? null;
const mode: DeltaMode = replacedItem ? "replace" : "add";
const deltas: Record<number, CandidateDelta> = {};
for (const candidate of candidates) {
let weightDelta: number | null = null;
let priceDelta: number | null = null;
if (candidate.weightGrams != null) {
weightDelta =
replacedItem?.weightGrams != null
? candidate.weightGrams - replacedItem.weightGrams
: candidate.weightGrams;
}
if (candidate.priceCents != null) {
priceDelta =
replacedItem?.priceCents != null
? candidate.priceCents - replacedItem.priceCents
: candidate.priceCents;
}
deltas[candidate.id] = {
candidateId: candidate.id,
mode,
weightDelta,
priceDelta,
replacedItemName: replacedItem?.name ?? null,
};
}
return { mode, deltas };
}