diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 3f24f63..b92e994 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -228,7 +228,10 @@ Plans: 1. User can add a catalog item to collection with one confirmation step (category picker + notes) 2. User can add catalog items as thread candidates instantly from search 3. Resolving a catalog-linked candidate creates a properly linked reference item in collection -**Plans**: TBD +**Plans:** 2 plans +Plans: +- [ ] 22-01-PLAN.md -- UIStore + sonner + AddToCollectionModal + overlay/detail page collection wiring +- [ ] 22-02-PLAN.md -- AddToThreadModal with thread picker + new thread flow + CATFLOW-06 verification **UI hint**: yes ### Phase 23: Manual Entry Fallback @@ -267,5 +270,5 @@ Plans: | 19. Reference Item Model & Tags Schema | v2.0 | 3/3 | Complete | 2026-04-05 | | 20. FAB & Full-Screen Catalog Search | v2.0 | 2/2 | Complete | 2026-04-06 | | 21. Item & Catalog Detail Pages | v2.0 | 1/1 | Complete | 2026-04-06 | -| 22. Add-from-Catalog & Thread Integration | v2.0 | 0/? | Not started | - | +| 22. Add-from-Catalog & Thread Integration | v2.0 | 0/2 | Not started | - | | 23. Manual Entry Fallback | v2.0 | 0/? | Not started | - | diff --git a/.planning/phases/22-add-from-catalog-thread-integration/22-01-PLAN.md b/.planning/phases/22-add-from-catalog-thread-integration/22-01-PLAN.md new file mode 100644 index 0000000..53dde51 --- /dev/null +++ b/.planning/phases/22-add-from-catalog-thread-integration/22-01-PLAN.md @@ -0,0 +1,384 @@ +--- +phase: 22-add-from-catalog-thread-integration +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/client/stores/uiStore.ts + - src/client/components/AddToCollectionModal.tsx + - src/client/components/CatalogSearchOverlay.tsx + - src/client/routes/global-items/$globalItemId.tsx + - src/client/routes/__root.tsx +autonomous: true +requirements: [CATFLOW-03] + +must_haves: + truths: + - "Clicking Add on a catalog search card in collection mode opens the AddToCollectionModal" + - "AddToCollectionModal shows category dropdown, optional notes, optional purchase price, and submit/cancel buttons" + - "Submitting the modal creates a reference item with globalItemId and personal fields" + - "Success toast appears after adding item to collection" + - "Clicking Add to Collection on the global item detail page opens the same modal" + artifacts: + - path: "src/client/components/AddToCollectionModal.tsx" + provides: "Add-to-collection confirmation modal" + min_lines: 80 + - path: "src/client/stores/uiStore.ts" + provides: "Modal state slices for addToCollectionModal and catalogSessionThreadId" + contains: "addToCollectionModal" + - path: "src/client/routes/__root.tsx" + provides: "Toaster and AddToCollectionModal rendered at root" + contains: "Toaster" + key_links: + - from: "src/client/components/CatalogSearchOverlay.tsx" + to: "src/client/stores/uiStore.ts" + via: "openAddToCollection call replacing handleAddStub" + pattern: "openAddToCollection" + - from: "src/client/components/AddToCollectionModal.tsx" + to: "/api/items" + via: "useCreateItem mutation with globalItemId" + pattern: "useCreateItem" + - from: "src/client/routes/global-items/$globalItemId.tsx" + to: "src/client/stores/uiStore.ts" + via: "openAddToCollection on button click" + pattern: "openAddToCollection" +--- + + +Wire the add-to-collection flow: install sonner for toasts, extend UIStore with modal states, build the AddToCollectionModal component, and replace the stub handler in CatalogSearchOverlay and global item detail page for collection mode. + +Purpose: CATFLOW-03 -- users can add catalog items to their collection as reference items with personal fields (category, notes, purchase price). +Output: Working add-to-collection flow from both catalog search and global item detail page. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/22-add-from-catalog-thread-integration/22-CONTEXT.md +@.planning/phases/22-add-from-catalog-thread-integration/22-RESEARCH.md + + + + +From src/shared/schemas.ts: +```typescript +export const createItemSchema = z.object({ + name: z.string().min(1, "Name is required"), + weightGrams: z.number().nonnegative().optional(), + priceCents: z.number().int().nonnegative().optional(), + categoryId: z.number().int().positive(), + notes: z.string().optional(), + productUrl: z.string().url().optional().or(z.literal("")), + imageFilename: z.string().optional(), + imageSourceUrl: z.string().url().optional().or(z.literal("")), + quantity: z.number().int().positive().optional(), + globalItemId: z.number().int().positive().optional(), + purchasePriceCents: z.number().int().nonnegative().optional(), +}); +``` + +From src/client/hooks/useItems.ts: +```typescript +export function useCreateItem() // mutationFn: (data: CreateItem) => apiPost("/api/items", data) +// onSuccess invalidates ["items"] and ["totals"] +``` + +From src/client/hooks/useCategories.ts: +```typescript +export function useCategories() // queryKey: ["categories"], returns Category[] +``` + +From src/client/stores/uiStore.ts (existing pattern): +```typescript +catalogSearchOpen: boolean; +catalogSearchMode: "collection" | "thread" | null; +openCatalogSearch: (mode: "collection" | "thread") => void; +closeCatalogSearch: () => void; +``` + +From src/client/components/CreateThreadModal.tsx (modal pattern): +```typescript +// Modal pattern: fixed inset-0 z-50, bg-black/50 backdrop, onClick={handleClose} +// Inner div: w-full max-w-md bg-white rounded-xl shadow-xl p-6 +// Form with local state, UIStore for open/close, mutation hook for submit +``` + +From CatalogSearchOverlay.tsx CardProps: +```typescript +interface CardProps { + item: { + id: number; + brand: string; + model: string; + category: string | null; + weightGrams: number | null; + priceCents: number | null; + imageUrl: string | null; + }; + onAdd: (e: React.MouseEvent) => void; + onCardClick: () => void; + weight: (g: number) => string; + price: (cents: number) => string; +} +``` + + + + + + + Task 1: UIStore extension + sonner setup + AddToCollectionModal + + src/client/stores/uiStore.ts + src/client/components/AddToCollectionModal.tsx + src/client/routes/__root.tsx + + + src/client/stores/uiStore.ts + src/client/components/CreateThreadModal.tsx + src/client/hooks/useItems.ts + src/client/hooks/useCategories.ts + src/shared/schemas.ts + src/client/routes/__root.tsx + + +**Step 1: Install sonner** +```bash +bun add sonner +``` + +**Step 2: Extend UIStore** (`src/client/stores/uiStore.ts`) + +Add to the `UIState` interface: +```typescript +// Add-to-collection modal (per D-20) +addToCollectionModal: { open: boolean; globalItemId: number | null; globalItemName: string | null }; +openAddToCollection: (globalItemId: number, globalItemName: string) => void; +closeAddToCollection: () => void; + +// Add-to-thread modal (per D-21) +addToThreadModal: { open: boolean; globalItemId: number | null; globalItemName: string | null }; +openAddToThread: (globalItemId: number, globalItemName: string) => void; +closeAddToThread: () => void; + +// Session thread tracking (per D-22) +catalogSessionThreadId: number | null; +setCatalogSessionThreadId: (id: number | null) => void; +``` + +Add to the `create` implementation: +```typescript +// Add-to-collection modal +addToCollectionModal: { open: false, globalItemId: null, globalItemName: null }, +openAddToCollection: (globalItemId, globalItemName) => + set({ addToCollectionModal: { open: true, globalItemId, globalItemName } }), +closeAddToCollection: () => + set({ addToCollectionModal: { open: false, globalItemId: null, globalItemName: null } }), + +// Add-to-thread modal +addToThreadModal: { open: false, globalItemId: null, globalItemName: null }, +openAddToThread: (globalItemId, globalItemName) => + set({ addToThreadModal: { open: true, globalItemId, globalItemName } }), +closeAddToThread: () => + set({ addToThreadModal: { open: false, globalItemId: null, globalItemName: null } }), + +// Session thread tracking +catalogSessionThreadId: null, +setCatalogSessionThreadId: (id) => set({ catalogSessionThreadId: id }), +``` + +Also update `closeCatalogSearch` to reset session thread (per D-22): +```typescript +closeCatalogSearch: () => + set({ catalogSearchOpen: false, catalogSearchMode: null, catalogSessionThreadId: null }), +``` + +**Step 3: Create AddToCollectionModal** (`src/client/components/AddToCollectionModal.tsx`) + +Follow CreateThreadModal pattern exactly (per D-05). Component structure: +- Read `addToCollectionModal` from UIStore (`open`, `globalItemId`, `globalItemName`) +- Read `closeAddToCollection` from UIStore +- Use `useCategories()` for category dropdown +- Use `useCreateItem()` for the mutation +- Local state: `categoryId` (number | null), `notes` (string), `purchasePriceCents` (number | undefined) +- Auto-match category: when categories load, find category where `c.name.toLowerCase() === globalItemCategory?.toLowerCase()`, fall back to `categories?.[0]?.id`. Since we only have `globalItemName` in UIStore (not category), skip auto-match for now -- default to first category. +- If `!open || !globalItemId` return null +- Render modal: `fixed inset-0 z-50 flex items-center justify-center bg-black/50` +- Inner form: `w-full max-w-md bg-white rounded-xl shadow-xl p-6` +- Header: "Add to Collection" h2 +- Show item name as a subheading: `

{globalItemName}

` +- Category dropdown (per D-02): `