`:
+ ```tsx
+
+
+
+ updateClassification.mutate({
+ itemId: item.id,
+ classification: nextClassification(item.classification),
+ })}
+ />
+
+
+ ```
+ - Alternatively, the badge can go inside the card's badge row if preferred. Use discretion on exact placement -- it should be near the weight/price badges but distinct.
+
+ 7. **Run all tests** to verify nothing broken.
+
+
+ bun test tests/routes/setups.test.ts && bun test tests/services/setup.service.test.ts
+
+
+ - PATCH /api/setups/:id/items/:itemId/classification endpoint works (200 for valid, 400 for invalid)
+ - ClassificationBadge renders on each item card in setup detail view with muted gray styling
+ - Clicking the badge cycles classification: base weight -> worn -> consumable -> base weight
+ - Badge click does NOT open the item edit panel (stopPropagation works)
+ - Classification change persists after page refresh
+ - GET /api/setups/:id returns classification field for each item
+
+
+
+
+
+
+```bash
+# All tests pass
+bun test
+
+# Classification service tests specifically
+bun test tests/services/setup.service.test.ts -t "classification"
+
+# Classification route tests specifically
+bun test tests/routes/setups.test.ts -t "classification"
+```
+
+
+
+- Classification badge visible on every item card in setup detail view (not hidden for default)
+- Click cycles through base weight -> worn -> consumable -> base weight
+- Badge uses muted gray styling (bg-gray-100 text-gray-600) consistent with Phase 8 status badges
+- Default classification is "base" for newly added items
+- syncSetupItems preserves classifications when items are added/removed
+- Same item in different setups can have different classifications
+- All existing tests continue to pass
+
+
+
diff --git a/.planning/phases/09-weight-classification-and-visualization/09-02-PLAN.md b/.planning/phases/09-weight-classification-and-visualization/09-02-PLAN.md
new file mode 100644
index 0000000..8ea737b
--- /dev/null
+++ b/.planning/phases/09-weight-classification-and-visualization/09-02-PLAN.md
@@ -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"
+---
+
+
+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.
+
+
+
+@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
+@/home/jlmak/.claude/get-shit-done/templates/summary.md
+
+
+
+@.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
+
+
+
+
+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)
+
+
+
+
+
+
+ Task 1: Install Recharts, create WeightSummaryCard, wire into setup detail page
+
+ src/client/components/WeightSummaryCard.tsx,
+ src/client/routes/setups/$setupId.tsx,
+ package.json
+
+
+ 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:**
+ ```
+
+
+
+
+
+
+
+
+
+
+
+ {chartData.map((entry, index) => (
+ |
+ ))}
+
+
+ } />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ **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 `` 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 `` and before the items-grouped-by-category `
`, within the `{itemCount > 0 && (...)}` block.
+
+ 4. **Verify**: Run `bun run build` to ensure no TypeScript errors and Recharts imports resolve correctly.
+
+
+ bun run build
+
+
+ - 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
+
+
+
+
+ Task 2: Visual verification of complete weight classification and visualization
+ N/A
+
+ 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.
+
+
+ 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
+
+
+ 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.
+
+ Visual verification by user following steps above
+ User confirms all classification badges, weight subtotals, donut chart, toggle, and tooltips work correctly
+ Type "approved" to complete Phase 9, or describe any issues to address
+
+
+
+
+
+```bash
+# Full test suite passes
+bun test
+
+# Build succeeds
+bun run build
+
+# Lint passes
+bun run lint
+```
+
+
+
+- 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
+
+
+