docs(09): create phase plan for weight classification and visualization

This commit is contained in:
2026-03-16 15:05:23 +01:00
parent 7d6cf31b05
commit 0e23996986
3 changed files with 673 additions and 4 deletions

View File

@@ -0,0 +1,309 @@
---
phase: 09-weight-classification-and-visualization
plan: 02
type: execute
wave: 2
depends_on: ["09-01"]
files_modified:
- src/client/components/WeightSummaryCard.tsx
- src/client/routes/setups/$setupId.tsx
- package.json
autonomous: false
requirements: [CLAS-02, VIZZ-01, VIZZ-02, VIZZ-03]
must_haves:
truths:
- "Setup detail view shows separate weight subtotals for base weight, worn weight, consumable weight, and total"
- "User can view a donut chart showing weight distribution by category in the setup"
- "User can toggle the chart between category breakdown and classification breakdown via pill toggle"
- "Hovering a chart segment shows category/classification name, weight in selected unit, and percentage"
- "Total weight displayed in the center of the donut hole"
artifacts:
- path: "src/client/components/WeightSummaryCard.tsx"
provides: "Summary card with weight subtotals, donut chart, pill toggle, and tooltips"
min_lines: 100
- path: "src/client/routes/setups/$setupId.tsx"
provides: "WeightSummaryCard rendered below sticky bar when setup has items"
- path: "package.json"
provides: "recharts dependency installed"
contains: "recharts"
key_links:
- from: "src/client/components/WeightSummaryCard.tsx"
to: "recharts"
via: "PieChart, Pie, Cell, Tooltip, Label, ResponsiveContainer imports"
pattern: "from.*recharts"
- from: "src/client/components/WeightSummaryCard.tsx"
to: "src/client/lib/formatters.ts"
via: "formatWeight for subtotals and tooltip display"
pattern: "formatWeight"
- from: "src/client/routes/setups/$setupId.tsx"
to: "src/client/components/WeightSummaryCard.tsx"
via: "WeightSummaryCard rendered with setup.items prop"
pattern: "WeightSummaryCard"
---
<objective>
Add the WeightSummaryCard component with classification weight subtotals, a donut chart for weight distribution, and a pill toggle for switching between category and classification views.
Purpose: Users need to visualize how weight is distributed across their setup -- both by gear category (shelter, sleep, cook) and by classification (base weight, worn, consumable). The donut chart with tooltips makes weight analysis intuitive.
Output: A summary card below the setup sticky bar showing Base | Worn | Consumable | Total weight columns alongside a donut chart with interactive tooltips, togglable between category and classification breakdowns.
</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/09-weight-classification-and-visualization/09-CONTEXT.md
@.planning/phases/09-weight-classification-and-visualization/09-RESEARCH.md
@.planning/phases/09-weight-classification-and-visualization/09-01-SUMMARY.md
<interfaces>
<!-- Key types and contracts from Plan 01. Executor uses these directly. -->
From src/client/hooks/useSetups.ts (after Plan 01):
```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; // "base" | "worn" | "consumable" -- added by Plan 01
}
```
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;
```
From src/client/hooks/useWeightUnit.ts:
```typescript
export function useWeightUnit(): WeightUnit;
```
From 09-RESEARCH.md (Recharts pattern):
```typescript
import { PieChart, Pie, Cell, Tooltip, Label, ResponsiveContainer } from "recharts";
// Use Cell for per-slice colors (still functional in v3, deprecated for v4)
// Use fixed numeric height on ResponsiveContainer (e.g., height={200})
// Filter out zero-weight entries before passing to chart
```
From 09-RESEARCH.md (color palettes):
```typescript
const CATEGORY_COLORS = [
"#6366f1", "#f59e0b", "#10b981", "#ef4444", "#8b5cf6",
"#06b6d4", "#f97316", "#ec4899", "#14b8a6", "#84cc16",
];
const CLASSIFICATION_COLORS = {
base: "#6366f1", // indigo
worn: "#f59e0b", // amber
consumable: "#10b981", // emerald
};
```
From 09-CONTEXT.md (locked decisions):
- Summary card below sticky bar, always visible when setup has items
- Card with columns layout: Base | Worn | Consumable | Total
- Donut chart inside the summary card alongside weight subtotals
- Pill toggle above the chart: "Category" / "Classification" (same style as weight unit selector)
- Total weight in center of donut hole
- Hover tooltips: segment name, weight in selected unit, percentage
- Chart library: Recharts (PieChart + Pie with innerRadius)
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Install Recharts, create WeightSummaryCard, wire into setup detail page</name>
<files>
src/client/components/WeightSummaryCard.tsx,
src/client/routes/setups/$setupId.tsx,
package.json
</files>
<action>
1. **Install Recharts**: Run `bun add recharts`. This adds recharts to package.json. React and react-dom are already peer deps in the project.
2. **Create WeightSummaryCard component** (`src/client/components/WeightSummaryCard.tsx`):
**Props interface:**
```typescript
interface WeightSummaryCardProps {
items: SetupItemWithCategory[]; // from useSetups hook (includes classification field)
}
```
Import `SetupItemWithCategory` from `../hooks/useSetups`.
**State:** `viewMode: "category" | "classification"` -- local React state, default "category".
**Weight subtotals computation** (derive from items array):
```typescript
const baseWeight = items.reduce((sum, i) => i.classification === "base" ? sum + (i.weightGrams ?? 0) : sum, 0);
const wornWeight = items.reduce((sum, i) => i.classification === "worn" ? sum + (i.weightGrams ?? 0) : sum, 0);
const consumableWeight = items.reduce((sum, i) => i.classification === "consumable" ? sum + (i.weightGrams ?? 0) : sum, 0);
const totalWeight = baseWeight + wornWeight + consumableWeight;
```
**Chart data transformation:**
- `buildCategoryChartData(items)`: Group by `categoryName`, sum `weightGrams`, compute percentage. Filter out zero-weight groups. Return `Array<{ name: string, weight: number, percent: number }>`.
- `buildClassificationChartData(items)`: Group by classification using labels ("Base Weight", "Worn", "Consumable"), sum weights, compute percentage. Filter out zero-weight groups.
- Select data source based on `viewMode`.
**Render structure:**
```
<div className="bg-white rounded-xl border border-gray-100 p-5 mb-6">
<!-- Pill toggle: Category | Classification -->
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-gray-700">Weight Summary</h3>
<PillToggle viewMode={viewMode} onChange={setViewMode} />
</div>
<!-- Main content: chart + subtotals side by side -->
<div className="flex items-center gap-8">
<!-- Donut chart -->
<div className="flex-shrink-0" style={{ width: 180, height: 180 }}>
<ResponsiveContainer width="100%" height={180}>
<PieChart>
<Pie data={chartData} dataKey="weight" nameKey="name"
cx="50%" cy="50%" innerRadius={55} outerRadius={80} paddingAngle={2}>
{chartData.map((entry, index) => (
<Cell key={entry.name} fill={colors[index % colors.length]} />
))}
<Label value={formatWeight(totalWeight, unit)} position="center"
style={{ fontSize: "14px", fontWeight: 600, fill: "#374151" }} />
</Pie>
<Tooltip content={<CustomTooltip unit={unit} />} />
</PieChart>
</ResponsiveContainer>
</div>
<!-- Weight subtotals columns -->
<div className="flex-1 grid grid-cols-4 gap-4">
<SubtotalColumn label="Base" weight={baseWeight} unit={unit} color="#6366f1" />
<SubtotalColumn label="Worn" weight={wornWeight} unit={unit} color="#f59e0b" />
<SubtotalColumn label="Consumable" weight={consumableWeight} unit={unit} color="#10b981" />
<SubtotalColumn label="Total" weight={totalWeight} unit={unit} />
</div>
</div>
</div>
```
**Pill toggle** (inline component or extracted):
- Two buttons in a `bg-gray-100 rounded-full` container: "Category" and "Classification".
- Active state: `bg-white text-gray-700 shadow-sm font-medium`. Inactive: `text-gray-400 hover:text-gray-600`.
- Same pattern as TotalsBar weight unit selector.
**SubtotalColumn** (inline component):
- Vertical stack: colored dot (if color provided), label in text-xs text-gray-500, weight value in text-sm font-semibold text-gray-900.
**CustomTooltip:**
- Props: `active`, `payload`, `unit` (WeightUnit).
- When active and payload exists, show: segment name (bold), weight formatted with `formatWeight()`, percentage as `(XX.X%)`.
- Styled: `bg-white border border-gray-200 rounded-lg shadow-lg px-3 py-2 text-sm`.
**Color selection:**
- When `viewMode === "category"`: use `CATEGORY_COLORS` array (cycle through for many categories).
- When `viewMode === "classification"`: use `CLASSIFICATION_COLORS` object (keyed by classification value).
**Edge cases:**
- If all items have null/zero weight, show a placeholder message ("No weight data to display") instead of the chart.
- If items array is empty, component should not render (handled by parent).
3. **Wire into setup detail page** (`src/client/routes/setups/$setupId.tsx`):
- Import `WeightSummaryCard` from `../../components/WeightSummaryCard`.
- Render `<WeightSummaryCard items={setup.items} />` between the actions bar and the items grid (before the `{itemCount > 0 && (` block), but INSIDE the `itemCount > 0` condition so it only shows when there are items.
- Exact placement: after the actions `<div>` and before the items-grouped-by-category `<div>`, within the `{itemCount > 0 && (...)}` block.
4. **Verify**: Run `bun run build` to ensure no TypeScript errors and Recharts imports resolve correctly.
</action>
<verify>
<automated>bun run build</automated>
</verify>
<done>
- WeightSummaryCard renders below sticky bar when setup has items
- Shows 4 columns: Base | Worn | Consumable | Total with correct weight values in selected unit
- Donut chart renders with colored segments for weight distribution
- Pill toggle switches between category view and classification view
- Hovering chart segments shows tooltip with name, weight, and percentage
- Total weight displayed in center of donut hole
- Empty/zero-weight items handled gracefully
- Build succeeds with no TypeScript errors
</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 2: Visual verification of complete weight classification and visualization</name>
<files>N/A</files>
<action>
Present the user with verification steps for the complete Phase 9 feature set.
This checkpoint covers both Plan 01 (classification badges) and Plan 02 (summary card + chart) together.
</action>
<what-built>
Complete weight classification and visualization system:
1. Classification badges on every item card in setup view (click to cycle: base weight / worn / consumable)
2. Weight summary card with Base | Worn | Consumable | Total subtotals
3. Donut chart with category/classification toggle and hover tooltips
4. Total weight in the center of the donut hole
</what-built>
<how-to-verify>
1. Start dev servers: `bun run dev:server` and `bun run dev:client`
2. Open http://localhost:5173 and navigate to a setup with items (or create one and add items)
3. **Classification badges**: Verify each item card shows a gray pill badge. Click it and confirm it cycles: "Base Weight" -> "Worn" -> "Consumable" -> "Base Weight". Confirm clicking the badge does NOT open the item edit panel.
4. **Classification persistence**: Refresh the page. Confirm classifications are preserved.
5. **Weight subtotals**: With items classified differently, verify the summary card shows correct subtotals for Base, Worn, Consumable, and Total columns.
6. **Donut chart (Category view)**: Verify the donut chart shows colored segments grouped by category. Hover segments to see tooltip with category name, weight, and percentage.
7. **Donut chart (Classification view)**: Click the "Classification" pill toggle. Verify chart segments change to show base/worn/consumable breakdown with different colors. Hover to verify tooltips.
8. **Donut center**: Confirm total weight is displayed in the center of the donut hole in the selected weight unit.
9. **Weight unit**: Toggle the weight unit in the top bar (if available). Confirm all subtotals, chart center, and tooltips update to the new unit.
10. **Add/remove items**: Add another item to the setup. Verify it appears with default "Base Weight" badge and the chart updates. Remove an item and verify classifications for remaining items are preserved.
</how-to-verify>
<verify>Visual verification by user following steps above</verify>
<done>User confirms all classification badges, weight subtotals, donut chart, toggle, and tooltips work correctly</done>
<resume-signal>Type "approved" to complete Phase 9, or describe any issues to address</resume-signal>
</task>
</tasks>
<verification>
```bash
# Full test suite passes
bun test
# Build succeeds
bun run build
# Lint passes
bun run lint
```
</verification>
<success_criteria>
- WeightSummaryCard visible below sticky bar on setup detail page (only when items exist)
- Four weight columns (Base, Worn, Consumable, Total) show correct values in selected unit
- Donut chart renders with colored segments proportional to weight distribution
- Pill toggle switches between category and classification chart views
- Tooltip on hover shows segment name, formatted weight, and percentage
- Total weight displayed in center of donut hole
- Chart handles edge cases (no weight data, single category, etc.)
- User confirms visual appearance matches expectations
</success_criteria>
<output>
After completion, create `.planning/phases/09-weight-classification-and-visualization/09-02-SUMMARY.md`
</output>