310 lines
15 KiB
Markdown
310 lines
15 KiB
Markdown
---
|
|
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>
|