This commit is contained in:
@@ -96,7 +96,10 @@ Plans:
|
||||
2. When the selected setup contains an item in the same category as the thread, the delta reflects replacing that item (negative delta is possible) rather than pure addition
|
||||
3. When no category match exists in the selected setup, the delta shows a pure addition amount clearly labeled as "add"
|
||||
4. A candidate with no weight recorded shows a "-- (no weight data)" indicator instead of a zero delta
|
||||
**Plans**: TBD
|
||||
**Plans:** 2 plans
|
||||
Plans:
|
||||
- [ ] 13-01-PLAN.md — TDD pure impact delta computation, uiStore state, ThreadWithCandidates type fix, useImpactDeltas hook
|
||||
- [ ] 13-02-PLAN.md — SetupImpactSelector + ImpactDeltaBadge components, wire into thread detail and all candidate views
|
||||
|
||||
## Progress
|
||||
|
||||
@@ -114,4 +117,4 @@ Plans:
|
||||
| 10. Schema Foundation + Pros/Cons Fields | v1.3 | 1/1 | Complete | 2026-03-16 |
|
||||
| 11. Candidate Ranking | 2/2 | Complete | 2026-03-16 | - |
|
||||
| 12. Comparison View | 1/1 | Complete | 2026-03-17 | - |
|
||||
| 13. Setup Impact Preview | v1.3 | 0/TBD | Not started | - |
|
||||
| 13. Setup Impact Preview | v1.3 | 0/2 | Not started | - |
|
||||
|
||||
253
.planning/phases/13-setup-impact-preview/13-01-PLAN.md
Normal file
253
.planning/phases/13-setup-impact-preview/13-01-PLAN.md
Normal file
@@ -0,0 +1,253 @@
|
||||
---
|
||||
phase: 13-setup-impact-preview
|
||||
plan: 01
|
||||
type: tdd
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/client/lib/impactDeltas.ts
|
||||
- src/client/hooks/useImpactDeltas.ts
|
||||
- src/client/hooks/useThreads.ts
|
||||
- src/client/stores/uiStore.ts
|
||||
- tests/lib/impactDeltas.test.ts
|
||||
autonomous: true
|
||||
requirements: [IMPC-01, IMPC-02, IMPC-03, IMPC-04]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "computeImpactDeltas returns replace-mode deltas when a setup item matches the thread category"
|
||||
- "computeImpactDeltas returns add-mode deltas (candidate value only) when no category match exists"
|
||||
- "computeImpactDeltas returns null weightDelta/priceDelta when candidate has null weight/price"
|
||||
- "computeImpactDeltas returns mode 'none' with empty deltas when setupItems is undefined"
|
||||
- "selectedSetupId state persists in uiStore and can be set/cleared"
|
||||
- "ThreadWithCandidates interface includes categoryId field"
|
||||
artifacts:
|
||||
- path: "src/client/lib/impactDeltas.ts"
|
||||
provides: "Pure computeImpactDeltas function with CandidateDelta, ImpactDeltas types"
|
||||
exports: ["computeImpactDeltas", "CandidateInput", "CandidateDelta", "DeltaMode", "ImpactDeltas"]
|
||||
- path: "src/client/hooks/useImpactDeltas.ts"
|
||||
provides: "React hook wrapping computeImpactDeltas in useMemo"
|
||||
exports: ["useImpactDeltas"]
|
||||
- path: "tests/lib/impactDeltas.test.ts"
|
||||
provides: "Unit tests for all four IMPC requirements"
|
||||
contains: "computeImpactDeltas"
|
||||
key_links:
|
||||
- from: "src/client/hooks/useImpactDeltas.ts"
|
||||
to: "src/client/lib/impactDeltas.ts"
|
||||
via: "import computeImpactDeltas"
|
||||
pattern: "import.*computeImpactDeltas.*from.*lib/impactDeltas"
|
||||
- from: "src/client/hooks/useImpactDeltas.ts"
|
||||
to: "src/client/hooks/useSetups.ts"
|
||||
via: "SetupItemWithCategory type import"
|
||||
pattern: "import.*SetupItemWithCategory.*from.*useSetups"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the pure impact delta computation logic with full TDD coverage, add selectedSetupId to uiStore, fix the ThreadWithCandidates type to include categoryId, and wrap it all in a useImpactDeltas hook.
|
||||
|
||||
Purpose: Establish the data layer and contracts that Plan 02 will consume for rendering delta indicators. TDD ensures the replace/add mode logic and null-weight handling are correct before any UI work.
|
||||
Output: Tested pure function, React hook, updated types, uiStore state.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jlmak/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/13-setup-impact-preview/13-RESEARCH.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||
|
||||
From src/client/hooks/useSetups.ts:
|
||||
```typescript
|
||||
interface SetupItemWithCategory {
|
||||
id: number;
|
||||
name: string;
|
||||
weightGrams: number | null;
|
||||
priceCents: number | null;
|
||||
categoryId: number;
|
||||
notes: string | null;
|
||||
productUrl: string | null;
|
||||
imageFilename: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
categoryName: string;
|
||||
categoryIcon: string;
|
||||
classification: string;
|
||||
}
|
||||
|
||||
export function useSetup(setupId: number | null) {
|
||||
return useQuery({
|
||||
queryKey: ["setups", setupId],
|
||||
queryFn: () => apiGet<SetupWithItems>(`/api/setups/${setupId}`),
|
||||
enabled: setupId != null, // CRITICAL: prevents null fetch
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
From src/client/hooks/useThreads.ts (BEFORE fix):
|
||||
```typescript
|
||||
interface ThreadWithCandidates {
|
||||
id: number;
|
||||
name: string;
|
||||
status: "active" | "resolved";
|
||||
resolvedCandidateId: number | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
candidates: CandidateWithCategory[];
|
||||
// NOTE: categoryId is MISSING — server returns it but type omits it
|
||||
}
|
||||
```
|
||||
|
||||
From src/client/stores/uiStore.ts (existing pattern):
|
||||
```typescript
|
||||
candidateViewMode: "list" | "grid" | "compare";
|
||||
setCandidateViewMode: (mode: "list" | "grid" | "compare") => void;
|
||||
// Add selectedSetupId + setter using same pattern
|
||||
```
|
||||
|
||||
From src/client/lib/formatters.ts:
|
||||
```typescript
|
||||
export type WeightUnit = "g" | "oz" | "lb" | "kg";
|
||||
export function formatWeight(grams: number | null | undefined, unit: WeightUnit = "g"): string;
|
||||
export type Currency = "USD" | "EUR" | "GBP" | "JPY" | "CAD" | "AUD";
|
||||
export function formatPrice(cents: number | null | undefined, currency: Currency = "USD"): string;
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<feature>
|
||||
<name>Impact Delta Computation</name>
|
||||
<files>src/client/lib/impactDeltas.ts, tests/lib/impactDeltas.test.ts, src/client/hooks/useImpactDeltas.ts, src/client/hooks/useThreads.ts, src/client/stores/uiStore.ts</files>
|
||||
<behavior>
|
||||
IMPC-01 (setup selected, deltas computed):
|
||||
- Given candidates with weight/price and a setup with items, returns per-candidate delta objects with weightDelta and priceDelta numbers
|
||||
- Given no setup selected (setupItems = undefined), returns { mode: "none", deltas: {} }
|
||||
|
||||
IMPC-02 (replace mode auto-detection):
|
||||
- Given setup items where one has categoryId === threadCategoryId, mode is "replace"
|
||||
- In replace mode, weightDelta = candidate.weightGrams - replacedItem.weightGrams
|
||||
- In replace mode, priceDelta = candidate.priceCents - replacedItem.priceCents
|
||||
- replacedItemName is populated with the matched item's name
|
||||
|
||||
IMPC-03 (add mode):
|
||||
- Given setup items where NONE have categoryId === threadCategoryId, mode is "add"
|
||||
- In add mode, weightDelta = candidate.weightGrams (pure addition)
|
||||
- In add mode, priceDelta = candidate.priceCents (pure addition)
|
||||
- replacedItemName is null
|
||||
|
||||
IMPC-04 (null weight handling):
|
||||
- Given candidate.weightGrams is null, weightDelta is null (not 0, not NaN)
|
||||
- Given candidate.priceCents is null, priceDelta is null
|
||||
- In replace mode with replacedItem.weightGrams null but candidate has weight, weightDelta = candidate.weightGrams (treat as add for that field)
|
||||
|
||||
Edge cases:
|
||||
- Empty candidates array -> returns { mode based on setup, deltas: {} }
|
||||
- Multiple setup items in same category as thread -> first match used for replacement
|
||||
</behavior>
|
||||
<implementation>
|
||||
1. Create src/client/lib/impactDeltas.ts with:
|
||||
- CandidateInput interface: { id: number; weightGrams: number | null; priceCents: number | null }
|
||||
- DeltaMode type: "replace" | "add" | "none"
|
||||
- CandidateDelta interface: { candidateId, mode, weightDelta, priceDelta, replacedItemName }
|
||||
- ImpactDeltas interface: { mode: DeltaMode; deltas: Record<number, CandidateDelta> }
|
||||
- SetupItemInput interface: { categoryId: number; weightGrams: number | null; priceCents: number | null; name: string } (minimal subset of SetupItemWithCategory)
|
||||
- computeImpactDeltas(candidates, setupItems, threadCategoryId) pure function
|
||||
|
||||
2. Create src/client/hooks/useImpactDeltas.ts wrapping in useMemo
|
||||
|
||||
3. Add categoryId to ThreadWithCandidates in useThreads.ts
|
||||
|
||||
4. Add selectedSetupId + setSelectedSetupId to uiStore.ts
|
||||
</implementation>
|
||||
</feature>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: TDD pure computeImpactDeltas function</name>
|
||||
<files>src/client/lib/impactDeltas.ts, tests/lib/impactDeltas.test.ts</files>
|
||||
<behavior>
|
||||
- Test: no setup selected (undefined) returns mode "none", empty deltas
|
||||
- Test: replace mode — setup item matches threadCategoryId, deltas are candidate minus replaced item
|
||||
- Test: add mode — no setup item matches, deltas equal candidate values
|
||||
- Test: null candidate weight returns null weightDelta, not zero
|
||||
- Test: null candidate price returns null priceDelta
|
||||
- Test: replace mode with null replacedItem weight but valid candidate weight returns candidate weight as delta (add-like for that field)
|
||||
- Test: negative delta in replace mode (candidate lighter than replaced item)
|
||||
- Test: zero delta in replace mode (identical weight)
|
||||
- Test: replacedItemName populated in replace mode, null in add mode
|
||||
</behavior>
|
||||
<action>
|
||||
RED: Create tests/lib/impactDeltas.test.ts importing computeImpactDeltas from @/client/lib/impactDeltas. Write all test cases above using Bun test (describe/test/expect). Run tests — they MUST fail (module not found).
|
||||
|
||||
GREEN: Create src/client/lib/impactDeltas.ts with:
|
||||
- Export types: CandidateInput, SetupItemInput, DeltaMode, CandidateDelta, ImpactDeltas
|
||||
- Export function computeImpactDeltas(candidates: CandidateInput[], setupItems: SetupItemInput[] | undefined, threadCategoryId: number): ImpactDeltas
|
||||
- Logic: if !setupItems return { mode: "none", deltas: {} }
|
||||
- Find replacedItem = setupItems.find(item => item.categoryId === threadCategoryId) ?? null
|
||||
- mode = replacedItem ? "replace" : "add"
|
||||
- For each candidate: null-guard weight/price BEFORE arithmetic. In replace mode with non-null replaced value, delta = candidate - replaced. In replace mode with null replaced value, delta = candidate value (like add). In add mode, delta = candidate value.
|
||||
- Run tests — all MUST pass.
|
||||
|
||||
REFACTOR: None needed for pure function.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun test tests/lib/impactDeltas.test.ts</automated>
|
||||
</verify>
|
||||
<done>All 9+ test cases pass. computeImpactDeltas correctly handles replace mode, add mode, null weights, null prices, and edge cases. Types are exported.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add uiStore state, fix ThreadWithCandidates type, create useImpactDeltas hook</name>
|
||||
<files>src/client/stores/uiStore.ts, src/client/hooks/useThreads.ts, src/client/hooks/useImpactDeltas.ts</files>
|
||||
<action>
|
||||
1. In src/client/stores/uiStore.ts, add to UIState interface:
|
||||
- selectedSetupId: number | null;
|
||||
- setSelectedSetupId: (id: number | null) => void;
|
||||
Add to create() initializer:
|
||||
- selectedSetupId: null,
|
||||
- setSelectedSetupId: (id) => set({ selectedSetupId: id }),
|
||||
Place after the "Candidate view mode" section as "// Setup impact preview" section.
|
||||
|
||||
2. In src/client/hooks/useThreads.ts, add `categoryId: number;` to the ThreadWithCandidates interface, after the `resolvedCandidateId` field. The server already returns this field — the type was simply missing it.
|
||||
|
||||
3. Create src/client/hooks/useImpactDeltas.ts:
|
||||
- Import useMemo from react
|
||||
- Import computeImpactDeltas and types from "../lib/impactDeltas"
|
||||
- Import type { SetupItemWithCategory } from "./useSetups"
|
||||
- Export function useImpactDeltas(candidates: CandidateInput[], setupItems: SetupItemWithCategory[] | undefined, threadCategoryId: number): ImpactDeltas
|
||||
- Body: return useMemo(() => computeImpactDeltas(candidates, setupItems, threadCategoryId), [candidates, setupItems, threadCategoryId])
|
||||
- Re-export CandidateDelta, DeltaMode, ImpactDeltas types for convenience
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun test tests/lib/impactDeltas.test.ts && bun test tests/lib/formatters.test.ts</automated>
|
||||
</verify>
|
||||
<done>uiStore has selectedSetupId + setter. ThreadWithCandidates includes categoryId. useImpactDeltas hook created and exports types. All existing tests still pass.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `bun test tests/lib/` passes all tests (impactDeltas + formatters)
|
||||
- `bun test` full suite passes (no regressions)
|
||||
- Types export correctly: CandidateInput, CandidateDelta, DeltaMode, ImpactDeltas, SetupItemInput from impactDeltas.ts
|
||||
- useImpactDeltas hook wraps pure function in useMemo
|
||||
- uiStore.selectedSetupId defaults to null
|
||||
- ThreadWithCandidates.categoryId is declared as number
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- All IMPC requirement behaviors are tested and passing via pure function unit tests
|
||||
- Data layer contracts (types, hook, uiStore state) are ready for Plan 02 UI consumption
|
||||
- Zero regressions in existing test suite
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/13-setup-impact-preview/13-01-SUMMARY.md`
|
||||
</output>
|
||||
330
.planning/phases/13-setup-impact-preview/13-02-PLAN.md
Normal file
330
.planning/phases/13-setup-impact-preview/13-02-PLAN.md
Normal file
@@ -0,0 +1,330 @@
|
||||
---
|
||||
phase: 13-setup-impact-preview
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["13-01"]
|
||||
files_modified:
|
||||
- src/client/components/SetupImpactSelector.tsx
|
||||
- src/client/components/ImpactDeltaBadge.tsx
|
||||
- src/client/routes/threads/$threadId.tsx
|
||||
- src/client/components/CandidateListItem.tsx
|
||||
- src/client/components/CandidateCard.tsx
|
||||
- src/client/components/ComparisonTable.tsx
|
||||
autonomous: true
|
||||
requirements: [IMPC-01, IMPC-02, IMPC-03, IMPC-04]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "User can select a setup from a dropdown in the thread header"
|
||||
- "Each candidate displays weight and cost delta badges when a setup is selected"
|
||||
- "Replace mode shows signed delta with replaced item name context"
|
||||
- "Add mode shows positive delta labeled as '(add)'"
|
||||
- "Candidate with no weight shows '-- (no weight data)' instead of a zero"
|
||||
- "Candidate with no price shows '-- (no price data)' instead of a zero"
|
||||
- "Deselecting setup ('None') clears all delta indicators"
|
||||
- "Deltas appear in list view, grid view, and comparison table"
|
||||
artifacts:
|
||||
- path: "src/client/components/SetupImpactSelector.tsx"
|
||||
provides: "Setup dropdown for thread header"
|
||||
exports: ["SetupImpactSelector"]
|
||||
- path: "src/client/components/ImpactDeltaBadge.tsx"
|
||||
provides: "Inline delta indicator component"
|
||||
exports: ["ImpactDeltaBadge"]
|
||||
- path: "src/client/routes/threads/$threadId.tsx"
|
||||
provides: "Thread detail page wired with impact preview"
|
||||
- path: "src/client/components/CandidateListItem.tsx"
|
||||
provides: "List item with delta badges"
|
||||
- path: "src/client/components/CandidateCard.tsx"
|
||||
provides: "Card with delta badges"
|
||||
- path: "src/client/components/ComparisonTable.tsx"
|
||||
provides: "Comparison table with impact delta rows"
|
||||
key_links:
|
||||
- from: "src/client/routes/threads/$threadId.tsx"
|
||||
to: "src/client/hooks/useImpactDeltas.ts"
|
||||
via: "useImpactDeltas hook call at page level"
|
||||
pattern: "useImpactDeltas"
|
||||
- from: "src/client/routes/threads/$threadId.tsx"
|
||||
to: "src/client/hooks/useSetups.ts"
|
||||
via: "useSetup(selectedSetupId) for setup item data"
|
||||
pattern: "useSetup\\(selectedSetupId"
|
||||
- from: "src/client/routes/threads/$threadId.tsx"
|
||||
to: "src/client/stores/uiStore.ts"
|
||||
via: "selectedSetupId state read"
|
||||
pattern: "useUIStore.*selectedSetupId"
|
||||
- from: "src/client/components/SetupImpactSelector.tsx"
|
||||
to: "src/client/stores/uiStore.ts"
|
||||
via: "setSelectedSetupId state write"
|
||||
pattern: "setSelectedSetupId"
|
||||
- from: "src/client/components/ImpactDeltaBadge.tsx"
|
||||
to: "src/client/lib/impactDeltas.ts"
|
||||
via: "CandidateDelta type import"
|
||||
pattern: "import.*CandidateDelta"
|
||||
- from: "src/client/components/ComparisonTable.tsx"
|
||||
to: "src/client/lib/impactDeltas.ts"
|
||||
via: "ImpactDeltas type for deltas prop"
|
||||
pattern: "import.*ImpactDeltas"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Build the UI components (setup dropdown + delta badges) and wire them into the thread detail page across all three view modes (list, grid, compare).
|
||||
|
||||
Purpose: This is the user-facing delivery of the impact preview feature. Plan 01 built the logic; this plan renders it.
|
||||
Output: SetupImpactSelector component, ImpactDeltaBadge component, updated CandidateListItem/CandidateCard/ComparisonTable with delta rendering, wired thread detail page.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jlmak/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/13-setup-impact-preview/13-RESEARCH.md
|
||||
@.planning/phases/13-setup-impact-preview/13-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Types created by Plan 01 that this plan consumes -->
|
||||
|
||||
From src/client/lib/impactDeltas.ts (created in Plan 01):
|
||||
```typescript
|
||||
export interface CandidateInput {
|
||||
id: number;
|
||||
weightGrams: number | null;
|
||||
priceCents: number | null;
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
```
|
||||
|
||||
From src/client/hooks/useImpactDeltas.ts (created in Plan 01):
|
||||
```typescript
|
||||
export function useImpactDeltas(
|
||||
candidates: CandidateInput[],
|
||||
setupItems: SetupItemWithCategory[] | undefined,
|
||||
threadCategoryId: number,
|
||||
): ImpactDeltas;
|
||||
```
|
||||
|
||||
From src/client/hooks/useSetups.ts:
|
||||
```typescript
|
||||
export function useSetups(): UseQueryResult<SetupListItem[]>;
|
||||
export function useSetup(setupId: number | null): UseQueryResult<SetupWithItems>;
|
||||
```
|
||||
|
||||
From src/client/stores/uiStore.ts (updated in Plan 01):
|
||||
```typescript
|
||||
selectedSetupId: number | null;
|
||||
setSelectedSetupId: (id: number | null) => void;
|
||||
```
|
||||
|
||||
From src/client/hooks/useThreads.ts (updated in Plan 01):
|
||||
```typescript
|
||||
interface ThreadWithCandidates {
|
||||
id: number;
|
||||
name: string;
|
||||
status: "active" | "resolved";
|
||||
resolvedCandidateId: number | null;
|
||||
categoryId: number; // <-- Added in Plan 01
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
candidates: CandidateWithCategory[];
|
||||
}
|
||||
```
|
||||
|
||||
From src/client/lib/formatters.ts:
|
||||
```typescript
|
||||
export function formatWeight(grams: number | null | undefined, unit: WeightUnit = "g"): string;
|
||||
export function formatPrice(cents: number | null | undefined, currency: Currency = "USD"): string;
|
||||
```
|
||||
|
||||
Existing component props that need delta additions:
|
||||
|
||||
CandidateListItem props:
|
||||
```typescript
|
||||
interface CandidateListItemProps {
|
||||
candidate: CandidateWithCategory;
|
||||
rank: number;
|
||||
isActive: boolean;
|
||||
onStatusChange: (status: "researching" | "ordered" | "arrived") => void;
|
||||
// Will add: delta?: CandidateDelta;
|
||||
}
|
||||
```
|
||||
|
||||
CandidateCard props:
|
||||
```typescript
|
||||
interface CandidateCardProps {
|
||||
id: number;
|
||||
name: string;
|
||||
weightGrams: number | null;
|
||||
priceCents: number | null;
|
||||
// ... other props
|
||||
// Will add: delta?: CandidateDelta;
|
||||
}
|
||||
```
|
||||
|
||||
ComparisonTable props:
|
||||
```typescript
|
||||
interface ComparisonTableProps {
|
||||
candidates: CandidateWithCategory[];
|
||||
resolvedCandidateId: number | null;
|
||||
// Will add: deltas?: Record<number, CandidateDelta>;
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create SetupImpactSelector and ImpactDeltaBadge components</name>
|
||||
<files>src/client/components/SetupImpactSelector.tsx, src/client/components/ImpactDeltaBadge.tsx</files>
|
||||
<action>
|
||||
1. Create src/client/components/SetupImpactSelector.tsx:
|
||||
- Import useSetups from hooks/useSetups
|
||||
- Import useUIStore from stores/uiStore
|
||||
- Export function SetupImpactSelector()
|
||||
- Read selectedSetupId and setSelectedSetupId from uiStore
|
||||
- Fetch setups via useSetups()
|
||||
- If no setups or loading, return null
|
||||
- Render: a flex row with label "Impact on setup:" (text-xs text-gray-500) and a native `<select>` element
|
||||
- Select value = selectedSetupId ?? "", onChange parses to number or null
|
||||
- Options: "None" (value="") + each setup by name
|
||||
- Styling: text-sm border border-gray-200 rounded-lg px-2 py-1 text-gray-700 bg-white focus:outline-none focus:ring-1 focus:ring-gray-300
|
||||
|
||||
2. Create src/client/components/ImpactDeltaBadge.tsx:
|
||||
- Import type CandidateDelta from lib/impactDeltas
|
||||
- Import formatWeight, formatPrice, WeightUnit, Currency from lib/formatters
|
||||
- Import useWeightUnit from hooks/useWeightUnit
|
||||
- Import useCurrency from hooks/useCurrency
|
||||
- Export function ImpactDeltaBadge({ delta, type }: { delta: CandidateDelta | undefined; type: "weight" | "price" })
|
||||
- If !delta or delta.mode === "none", return null
|
||||
- Pick value: type === "weight" ? delta.weightDelta : delta.priceDelta
|
||||
- If value === null (no data): render `<span className="text-xs text-gray-300">` with "-- (no weight data)" or "-- (no price data)" depending on type
|
||||
- If value is a number:
|
||||
- formatted = type === "weight" ? formatWeight(Math.abs(value), unit) : formatPrice(Math.abs(value), currency)
|
||||
- sign: value > 0 -> "+" , value < 0 -> "-" (use minus sign), value === 0 -> +/-
|
||||
- colorClass: value < 0 -> "text-green-600" (lighter/cheaper is good), value > 0 -> "text-red-500", value === 0 -> "text-gray-400"
|
||||
- modeLabel: delta.mode === "add" ? " (add)" : ""
|
||||
- vsLabel: delta.mode === "replace" && delta.replacedItemName ? ` vs ${delta.replacedItemName}` : "" (only show this as a title attribute on the span, not inline text -- too long)
|
||||
- Render: `<span className="text-xs font-medium {colorClass}" title={vsLabel || undefined}>{sign}{formatted}{modeLabel}</span>`
|
||||
- The component reads unit/currency internally via hooks so callers don't need to pass them.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run lint 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
<done>SetupImpactSelector renders a setup dropdown reading from uiStore. ImpactDeltaBadge renders signed, colored delta indicators with null-data handling. Both components lint-clean.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Wire impact preview into thread detail page and all candidate views</name>
|
||||
<files>src/client/routes/threads/$threadId.tsx, src/client/components/CandidateListItem.tsx, src/client/components/CandidateCard.tsx, src/client/components/ComparisonTable.tsx</files>
|
||||
<action>
|
||||
1. In src/client/routes/threads/$threadId.tsx:
|
||||
- Add imports: useSetup from hooks/useSetups, useImpactDeltas from hooks/useImpactDeltas, SetupImpactSelector from components/SetupImpactSelector, type CandidateDelta from lib/impactDeltas
|
||||
- Read selectedSetupId from useUIStore: `const selectedSetupId = useUIStore((s) => s.selectedSetupId);`
|
||||
- Fetch setup data: `const { data: setupData } = useSetup(selectedSetupId ?? null);`
|
||||
- Compute deltas: `const impactDeltas = useImpactDeltas(thread.candidates, setupData?.items, thread.categoryId);` (place after thread is loaded, inside the render body after the isLoading/isError guards)
|
||||
- Place `<SetupImpactSelector />` in the header section, after the thread name/status row and before the toolbar. Wrap it in a div for spacing if needed.
|
||||
- Pass delta to CandidateListItem: add prop `delta={impactDeltas.deltas[candidate.id]}` to each CandidateListItem (both Reorder.Group and static div renderings)
|
||||
- Pass delta to CandidateCard: add prop `delta={impactDeltas.deltas[candidate.id]}` (the CandidateCard receives individual props, so pass it as `delta={impactDeltas.deltas[candidate.id]}`)
|
||||
- Pass deltas to ComparisonTable: add prop `deltas={impactDeltas.deltas}` alongside existing candidates and resolvedCandidateId
|
||||
|
||||
2. In src/client/components/CandidateListItem.tsx:
|
||||
- Import type CandidateDelta from lib/impactDeltas
|
||||
- Import ImpactDeltaBadge from ./ImpactDeltaBadge
|
||||
- Add `delta?: CandidateDelta;` to CandidateListItemProps
|
||||
- Add `delta` to destructured props
|
||||
- Render two ImpactDeltaBadge components inside the badges flex-wrap div (after the existing weight and price badges):
|
||||
- `{delta && <ImpactDeltaBadge delta={delta} type="weight" />}`
|
||||
- `{delta && <ImpactDeltaBadge delta={delta} type="price" />}`
|
||||
- Place these AFTER the existing weight/price badges so they appear as secondary indicators
|
||||
|
||||
3. In src/client/components/CandidateCard.tsx:
|
||||
- Import type CandidateDelta from lib/impactDeltas
|
||||
- Import ImpactDeltaBadge from ./ImpactDeltaBadge
|
||||
- Add `delta?: CandidateDelta;` to CandidateCardProps
|
||||
- Add `delta` to destructured props
|
||||
- Render two ImpactDeltaBadge components inside the badges flex-wrap div (after weight/price badges):
|
||||
- `{delta && <ImpactDeltaBadge delta={delta} type="weight" />}`
|
||||
- `{delta && <ImpactDeltaBadge delta={delta} type="price" />}`
|
||||
|
||||
4. In src/client/components/ComparisonTable.tsx:
|
||||
- Import type CandidateDelta from lib/impactDeltas
|
||||
- Import ImpactDeltaBadge from ./ImpactDeltaBadge
|
||||
- Add `deltas?: Record<number, CandidateDelta>;` to ComparisonTableProps
|
||||
- Add `deltas` to destructured props
|
||||
- Add two new rows to ATTRIBUTE_ROWS array, placed right after the "weight" row and "price" row respectively:
|
||||
a. After "weight" row, add:
|
||||
```
|
||||
{
|
||||
key: "impact-weight",
|
||||
label: "Impact (wt)",
|
||||
render: (c) => deltas?.[c.id] ? <ImpactDeltaBadge delta={deltas[c.id]} type="weight" /> : <span className="text-gray-300">--</span>,
|
||||
}
|
||||
```
|
||||
b. After "price" row, add:
|
||||
```
|
||||
{
|
||||
key: "impact-price",
|
||||
label: "Impact ($)",
|
||||
render: (c) => deltas?.[c.id] ? <ImpactDeltaBadge delta={deltas[c.id]} type="price" /> : <span className="text-gray-300">--</span>,
|
||||
}
|
||||
```
|
||||
- These are separate rows (per research recommendation) to avoid conflating candidate-relative deltas with setup impact deltas.
|
||||
- The impact rows show "--" when no setup is selected (deltas undefined or no entry for candidate).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun test && bun run lint 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- SetupImpactSelector dropdown visible in thread header
|
||||
- Selecting a setup shows weight/cost delta badges on each candidate in list, grid, and compare views
|
||||
- Replace mode: signed delta with green (lighter/cheaper) or red (heavier/pricier) coloring
|
||||
- Add mode: positive delta with "(add)" label
|
||||
- Null weight/price: shows "-- (no weight data)" / "-- (no price data)" indicator
|
||||
- Deselecting setup clears all delta indicators
|
||||
- ComparisonTable has dedicated "Impact (wt)" and "Impact ($)" rows
|
||||
- All tests pass, lint clean
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `bun test` full suite passes
|
||||
- `bun run lint` clean
|
||||
- SetupImpactSelector renders in thread header with all setups as options
|
||||
- Selecting a setup triggers useSetup fetch and delta computation
|
||||
- CandidateListItem, CandidateCard, ComparisonTable all render delta badges
|
||||
- Replace mode detected when setup has item in same category as thread
|
||||
- Add mode used otherwise
|
||||
- Null weight/price shows clear indicator
|
||||
- Deselecting shows no deltas (clean state)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- All four IMPC requirements visible in the UI
|
||||
- Delta rendering works across list, grid, and compare views
|
||||
- No regressions in existing functionality
|
||||
- Clean lint output
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/13-setup-impact-preview/13-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user