Files

17 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
09-weight-classification-and-visualization 01 execute 1
src/db/schema.ts
src/shared/schemas.ts
src/shared/types.ts
src/server/services/setup.service.ts
src/server/routes/setups.ts
src/client/lib/api.ts
src/client/hooks/useSetups.ts
src/client/components/ClassificationBadge.tsx
src/client/routes/setups/$setupId.tsx
tests/helpers/db.ts
tests/services/setup.service.test.ts
tests/routes/setups.test.ts
true
CLAS-01
CLAS-03
CLAS-04
truths artifacts key_links
User can click a classification badge on any item card within a setup and it cycles through base weight, worn, consumable
Items default to base weight classification when added to a setup
Same item in different setups can have different classifications
Classifications persist after adding/removing other items from the setup (syncSetupItems preserves them)
path provides contains
src/db/schema.ts classification column on setupItems table classification.*text.*default.*base
path provides exports
src/shared/schemas.ts classificationSchema Zod enum and updateClassificationSchema
classificationSchema
updateClassificationSchema
path provides exports
src/server/services/setup.service.ts updateItemClassification service function, classification-preserving syncSetupItems, classification field in getSetupWithItems
updateItemClassification
path provides
src/server/routes/setups.ts PATCH /:id/items/:itemId/classification endpoint
path provides min_lines
src/client/components/ClassificationBadge.tsx Click-to-cycle classification badge component 30
path provides
src/client/routes/setups/$setupId.tsx ClassificationBadge wired into item cards in setup view
path provides
tests/services/setup.service.test.ts Tests for updateItemClassification, classification preservation, defaults
path provides
tests/routes/setups.test.ts Integration test for PATCH classification route
from to via pattern
src/client/components/ClassificationBadge.tsx /api/setups/:id/items/:itemId/classification useUpdateItemClassification mutation hook apiPatch.*classification
from to via pattern
src/server/routes/setups.ts src/server/services/setup.service.ts updateItemClassification service call updateItemClassification
from to via pattern
src/server/services/setup.service.ts src/db/schema.ts setupItems.classification column setupItems.classification
from to via pattern
src/client/routes/setups/$setupId.tsx src/client/components/ClassificationBadge.tsx ClassificationBadge rendered on each ItemCard ClassificationBadge
Add per-setup item classification (base weight / worn / consumable) as a complete vertical slice: schema migration, service layer with tests, API route, and ClassificationBadge UI component wired into the setup detail page.

Purpose: Users need to classify gear items by their role within a specific setup to enable weight breakdown analysis. The same item can serve different roles in different setups (e.g., a jacket is "worn" in a hiking setup but "base weight" in a bike setup).

Output: Working classification system -- clicking a badge on any item card in a setup cycles through base/worn/consumable, persists to the database, and survives item sync operations.

<execution_context> @/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md @/home/jlmak/.claude/get-shit-done/templates/summary.md </execution_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

From src/db/schema.ts (setupItems table -- CURRENT, needs classification column added):

export const setupItems = sqliteTable("setup_items", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  setupId: integer("setup_id").notNull().references(() => setups.id, { onDelete: "cascade" }),
  itemId: integer("item_id").notNull().references(() => items.id, { onDelete: "cascade" }),
});

From src/server/services/setup.service.ts (functions to modify):

type Db = typeof prodDb;
export function getSetupWithItems(db: Db, setupId: number): { ...setup, items: [...] } | null;
export function syncSetupItems(db: Db, setupId: number, itemIds: number[]): void;
export function removeSetupItem(db: Db, setupId: number, itemId: number): void;

From src/shared/schemas.ts (existing pattern for enums):

export const candidateStatusSchema = z.enum(["researching", "ordered", "arrived"]);

From src/client/lib/api.ts (existing helpers -- NO apiPatch exists):

export async function apiGet<T>(url: string): Promise<T>;
export async function apiPost<T>(url: string, body: unknown): Promise<T>;
export async function apiPut<T>(url: string, body: unknown): Promise<T>;
export async function apiDelete<T>(url: string): Promise<T>;

From src/client/hooks/useSetups.ts (existing types):

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;
}
// NEEDS: classification field added to this interface

From src/client/components/StatusBadge.tsx (pattern reference for click interaction):

// Uses click-to-open popup with status options
// ClassificationBadge should be SIMPLER: direct click-to-cycle (only 3 values)
// Must call e.stopPropagation() to prevent ItemCard click handler

From src/client/components/ItemCard.tsx (props interface -- badge goes in the badges area):

interface ItemCardProps {
  id: number; name: string; weightGrams: number | null; priceCents: number | null;
  categoryName: string; categoryIcon: string; imageFilename: string | null;
  productUrl?: string | null; onRemove?: () => void;
}
// Classification badge will be rendered OUTSIDE ItemCard, in the setup detail page's
// grid layout, alongside the ItemCard. The ItemCard itself does NOT need modification.
// The badge sits in the flex-wrap gap-1.5 area of ItemCard OR as a sibling element.

From tests/helpers/db.ts (setup_items CREATE TABLE -- needs classification column):

CREATE TABLE setup_items (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  setup_id INTEGER NOT NULL REFERENCES setups(id) ON DELETE CASCADE,
  item_id INTEGER NOT NULL REFERENCES items(id) ON DELETE CASCADE
)
Task 1: Schema migration, service layer, and tests for classification src/db/schema.ts, src/shared/schemas.ts, src/shared/types.ts, src/server/services/setup.service.ts, tests/helpers/db.ts, tests/services/setup.service.test.ts - Test: updateItemClassification sets classification for a specific item in a specific setup - Test: updateItemClassification with "worn" changes item from default "base" to "worn" - Test: getSetupWithItems returns classification field for each item (defaults to "base") - Test: syncSetupItems preserves existing classifications when re-syncing (save before delete, restore after insert) - Test: syncSetupItems assigns "base" to newly added items that have no prior classification - Test: same item in two different setups can have different classifications 1. **Update test helper FIRST** (`tests/helpers/db.ts`): Add `classification text NOT NULL DEFAULT 'base'` to the `setup_items` CREATE TABLE statement.
2. **Write failing tests** in `tests/services/setup.service.test.ts`:
   - Add `describe("updateItemClassification", ...)` block with tests for setting classification and verifying the update
   - Add test in existing `getSetupWithItems` describe for classification field presence (should default to "base")
   - Add test in existing `syncSetupItems` describe for classification preservation (sync with different item list, verify classifications retained for items that remain)
   - Add test for same item in two setups having different classifications
   - Import the new `updateItemClassification` function from setup.service.ts

3. **Run tests** -- they must FAIL (RED phase).

4. **Update Drizzle schema** (`src/db/schema.ts`): Add `classification: text("classification").notNull().default("base")` to the `setupItems` table definition.

5. **Generate migration**: Run `bun run db:generate` to create the migration SQL file. Then run `bun run db:push` to apply.

6. **Add Zod schema** (`src/shared/schemas.ts`):
   ```typescript
   export const classificationSchema = z.enum(["base", "worn", "consumable"]);
   export const updateClassificationSchema = z.object({
     classification: classificationSchema,
   });
   ```

7. **Add types** (`src/shared/types.ts`): Add `UpdateClassification` type inferred from `updateClassificationSchema`. The `SetupItem` type auto-updates from Drizzle schema inference.

8. **Implement service functions** (`src/server/services/setup.service.ts`):
   - Add `updateItemClassification(db, setupId, itemId, classification)` -- uses `db.update(setupItems).set({ classification }).where(sql\`..setupId AND ..itemId\`)`.
   - Modify `getSetupWithItems` to include `classification: setupItems.classification` in the select fields.
   - Modify `syncSetupItems` to preserve classifications using Approach A from research: before deleting, read existing classifications into a `Map<number, string>` (itemId -> classification). After re-inserting, apply saved classifications using `classificationMap.get(itemId) ?? "base"` in the insert values.

9. **Run tests** -- they must PASS (GREEN phase).
bun test tests/services/setup.service.test.ts - updateItemClassification changes an item's classification in a setup - getSetupWithItems returns classification field defaulting to "base" - syncSetupItems preserves classifications for retained items, defaults new items to "base" - Same item can have different classifications in different setups - All existing setup service tests still pass Task 2: API route, client hook, ClassificationBadge, and wiring into setup detail page src/server/routes/setups.ts, src/client/lib/api.ts, src/client/hooks/useSetups.ts, src/client/components/ClassificationBadge.tsx, src/client/routes/setups/$setupId.tsx, tests/routes/setups.test.ts 1. **Add PATCH route** (`src/server/routes/setups.ts`): - Import `updateClassificationSchema` from schemas and `updateItemClassification` from service. - Add `app.patch("/:id/items/:itemId/classification", zValidator("json", updateClassificationSchema), handler)`. - Handler: extract `setupId` and `itemId` from params, `classification` from validated body, call `updateItemClassification(db, setupId, itemId, classification)`, return `{ success: true }`.
2. **Add integration test** (`tests/routes/setups.test.ts`):
   - Add `describe("PATCH /api/setups/:id/items/:itemId/classification", ...)` block.
   - Test: create setup, add item, PATCH classification to "worn", GET setup and verify item has classification "worn".
   - Test: PATCH with invalid classification value returns 400.

3. **Add `apiPatch` helper** (`src/client/lib/api.ts`):
   ```typescript
   export async function apiPatch<T>(url: string, body: unknown): Promise<T> {
     const res = await fetch(url, {
       method: "PATCH",
       headers: { "Content-Type": "application/json" },
       body: JSON.stringify(body),
     });
     return handleResponse<T>(res);
   }
   ```

4. **Update client hooks** (`src/client/hooks/useSetups.ts`):
   - Add `classification: string` field to `SetupItemWithCategory` interface (defaults to "base" from API).
   - Add `useUpdateItemClassification(setupId: number)` mutation hook:
     ```typescript
     export function useUpdateItemClassification(setupId: number) {
       const queryClient = useQueryClient();
       return useMutation({
         mutationFn: ({ itemId, classification }: { itemId: number; classification: string }) =>
           apiPatch<{ success: boolean }>(
             `/api/setups/${setupId}/items/${itemId}/classification`,
             { classification },
           ),
         onSuccess: () => {
           queryClient.invalidateQueries({ queryKey: ["setups", setupId] });
         },
       });
     }
     ```
   - Import `apiPatch` from `../lib/api`.

5. **Create ClassificationBadge component** (`src/client/components/ClassificationBadge.tsx`):
   - Props: `classification: string`, `onCycle: () => void`.
   - Define `CLASSIFICATION_ORDER = ["base", "worn", "consumable"] as const`.
   - Define `CLASSIFICATION_LABELS = { base: "Base Weight", worn: "Worn", consumable: "Consumable" }`.
   - Render as a `<button>` with pill styling: `bg-gray-100 text-gray-600 hover:bg-gray-200` (muted gray per user decision).
   - Display the label text for the current classification.
   - On click: call `e.stopPropagation()` (critical -- prevents ItemCard from opening edit panel), then call `onCycle()`.
   - The parent component computes the next classification and calls the mutation.

6. **Wire into setup detail page** (`src/client/routes/setups/$setupId.tsx`):
   - Import `ClassificationBadge` and `useUpdateItemClassification`.
   - Create the mutation hook: `const updateClassification = useUpdateItemClassification(numericId)`.
   - Add a helper function to compute next classification:
     ```typescript
     function nextClassification(current: string): string {
       const order = ["base", "worn", "consumable"];
       const idx = order.indexOf(current);
       return order[(idx + 1) % order.length];
     }
     ```
   - In the items grid, render `ClassificationBadge` below each `ItemCard` (as a sibling within the grid cell). Wrap ItemCard + badge in a `<div>`:
     ```tsx
     <div key={item.id}>
       <ItemCard ... />
       <div className="px-4 pb-3 -mt-1">
         <ClassificationBadge
           classification={item.classification}
           onCycle={() => updateClassification.mutate({
             itemId: item.id,
             classification: nextClassification(item.classification),
           })}
         />
       </div>
     </div>
     ```
   - 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"

</verification>

<success_criteria>
- 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
</success_criteria>

<output>
After completion, create `.planning/phases/09-weight-classification-and-visualization/09-01-SUMMARY.md`
</output>