docs(23): create phase plan for manual entry fallback

This commit is contained in:
2026-04-06 17:34:14 +02:00
parent d73da67cff
commit cca99778a4
2 changed files with 448 additions and 1 deletions

View File

@@ -242,7 +242,9 @@ Plans:
1. User can fall back to manual entry from catalog search via "Add Manually" link
2. Manual entry saves a standalone collection item (no globalItemId)
3. "Submit to catalog?" prompt appears after manual save but takes no backend action
**Plans**: TBD
**Plans:** 1 plan
Plans:
- [ ] 23-01-PLAN.md -- ManualEntryForm + CatalogSearchOverlay wiring
**UI hint**: yes
## Progress

View File

@@ -0,0 +1,445 @@
---
phase: 23-manual-entry-fallback
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- src/client/components/ManualEntryForm.tsx
- src/client/components/CatalogSearchOverlay.tsx
autonomous: true
requirements:
- CATFLOW-07
- CATFLOW-08
must_haves:
truths:
- "User can click 'Add Manually' from catalog search empty state to enter manual entry mode"
- "User can click a persistent 'Add Manually' link below search results to enter manual entry mode"
- "User sees a compact form with name (required), category, weight, price, purchase price, notes, product link, and image upload fields"
- "User can submit the form to create a standalone collection item (no globalItemId)"
- "After saving, user sees a success card with 'Submit to Catalog?' button, 'Add Another', and 'Done'"
- "'Submit to Catalog?' button shows a 'Coming soon' toast and takes no backend action"
- "Back arrow returns from manual form to search results without closing the overlay"
- "Search query auto-populates the item name field when entering manual mode"
artifacts:
- path: "src/client/components/ManualEntryForm.tsx"
provides: "Compact manual item entry form"
exports: ["ManualEntryForm"]
- path: "src/client/components/CatalogSearchOverlay.tsx"
provides: "Entry points, mode switching, success card rendering"
contains: "manualEntryMode"
key_links:
- from: "src/client/components/CatalogSearchOverlay.tsx"
to: "src/client/components/ManualEntryForm.tsx"
via: "conditional render when manualEntryMode && !savedItemName"
pattern: "manualEntryMode.*ManualEntryForm"
- from: "src/client/components/ManualEntryForm.tsx"
to: "src/client/hooks/useItems.ts"
via: "useCreateItem() mutation"
pattern: "useCreateItem"
- from: "src/client/components/CatalogSearchOverlay.tsx"
to: "sonner toast"
via: "Submit to Catalog button"
pattern: 'toast.*Coming soon'
---
<objective>
Add manual item entry fallback to the catalog search flow so users can add gear not found in the catalog.
Purpose: Complete the catalog-driven gear flow by ensuring users are never stuck when a catalog item doesn't exist. The "Add Manually" path creates standalone collection items and plants the seed for future catalog submissions.
Output: ManualEntryForm component + CatalogSearchOverlay wired with entry points, inline form rendering, and post-save success card.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/23-manual-entry-fallback/23-CONTEXT.md
@.planning/phases/23-manual-entry-fallback/23-RESEARCH.md
@src/client/components/CatalogSearchOverlay.tsx
@src/client/components/AddToCollectionModal.tsx
@src/client/components/ItemForm.tsx
@src/client/hooks/useItems.ts
@src/client/hooks/useCategories.ts
@src/shared/schemas.ts
<interfaces>
<!-- Key types and contracts the executor needs. -->
From src/client/hooks/useItems.ts:
```typescript
export function useCreateItem(): UseMutationResult<ItemWithCategory, Error, CreateItem>;
// CreateItem comes from src/shared/types.ts, inferred from createItemSchema
// globalItemId is optional — omitting it creates a standalone item
```
From src/shared/schemas.ts:
```typescript
export const createItemSchema = z.object({
name: z.string().min(1).max(200),
categoryId: z.number().int().positive(),
weightGrams: z.number().int().nonnegative().optional(),
priceCents: z.number().int().nonnegative().optional(),
notes: z.string().max(2000).optional(),
productUrl: z.string().url().max(500).optional(),
imageFilename: z.string().optional(),
globalItemId: z.number().int().positive().optional(),
purchasePriceCents: z.number().int().nonnegative().optional(),
quantity: z.number().int().positive().optional(),
});
```
From src/client/components/CatalogSearchOverlay.tsx:
```typescript
// Local state pattern — all view state managed via useState
const [searchInput, setSearchInput] = useState("");
const catalogSearchOpen = useUIStore((s) => s.catalogSearchOpen);
const catalogSearchMode = useUIStore((s) => s.catalogSearchMode);
const closeCatalogSearch = useUIStore((s) => s.closeCatalogSearch);
// EmptyState at line 667 — currently no onAddManually prop
function EmptyState({ hasQuery }: { hasQuery: boolean }) { ... }
// Reset effect at line 76 — must add new state resets here
useEffect(() => { if (!catalogSearchOpen) { /* resets */ } }, [catalogSearchOpen]);
```
From src/client/components/CategoryPicker.tsx:
```typescript
interface CategoryPickerProps {
value: number;
onChange: (categoryId: number) => void;
}
```
From src/client/components/ImageUpload.tsx:
```typescript
interface ImageUploadProps {
value: string | null;
onChange: (filename: string | null) => void;
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create ManualEntryForm component</name>
<files>src/client/components/ManualEntryForm.tsx</files>
<read_first>
src/client/components/AddToCollectionModal.tsx
src/client/components/ItemForm.tsx
src/client/hooks/useItems.ts
src/client/hooks/useCategories.ts
src/shared/schemas.ts
src/client/components/CategoryPicker.tsx
src/client/components/ImageUpload.tsx
</read_first>
<action>
Create `src/client/components/ManualEntryForm.tsx` — a compact, focused form for adding items manually (per D-05, D-06, D-07).
**Component interface (per D-05):**
```typescript
interface ManualEntryFormProps {
initialName?: string; // Auto-populated from search query
onSuccess: (itemName: string) => void; // Called after successful save
onBack: () => void; // Returns to search results (per D-04)
}
```
**Form state fields (per D-06):**
- `name` (string, required) — initialized from `initialName` prop
- `categoryId` (number | null) — pre-select first category once loaded (follow AddToCollectionModal pattern to avoid categoryId=0 Zod error)
- `weightGrams` (string, for input) — plain number input, converted to integer on submit
- `priceDollars` (string, for input) — plain number input, converted to cents via `Math.round(Number(val) * 100)`
- `purchasePrice` (string, for input) — same cents conversion
- `notes` (string)
- `productUrl` (string)
- `imageFilename` (string | null) — managed via `ImageUpload` component
- `error` (string | null) — validation/API error display
**Hooks to use:**
- `useCategories()` from `@/client/hooks/useCategories` for CategoryPicker data
- `useCreateItem()` from `@/client/hooks/useItems` for the mutation
**Form layout (top to bottom):**
1. Back arrow row: `<button>` with `ArrowLeft` icon (from lucide-react) + "Add Manually" title text — clicking calls `onBack` (per D-04)
2. Name input (text, required, full width)
3. Category picker via `<CategoryPicker value={categoryId} onChange={setCategoryId} />`
4. Two-column row: Weight (grams) input | Price input (dollars, label says "MSRP")
5. Purchase price input (dollars, label says "Purchase Price")
6. Product URL input (text)
7. Notes textarea (3 rows)
8. ImageUpload component
9. Error message display (red text, conditionally shown)
10. Submit button: "Add to Collection" — disabled when `createItem.isPending` or `!name.trim()` or `categoryId === null`
**Category initialization pattern (from AddToCollectionModal):**
```typescript
const { data: categories } = useCategories();
useEffect(() => {
if (categories && categories.length > 0 && categoryId === null) {
setCategoryId(categories[0].id);
}
}, [categories, categoryId]);
```
**Submit handler (per D-08):**
- Validate name is non-empty, categoryId is not null
- Call `createItem.mutate()` with:
- `name: name.trim()`
- `categoryId`
- `weightGrams: weightGrams ? Number(weightGrams) : undefined`
- `priceCents: priceDollars ? Math.round(Number(priceDollars) * 100) : undefined`
- `purchasePriceCents: purchasePrice ? Math.round(Number(purchasePrice) * 100) : undefined`
- `notes: notes || undefined`
- `productUrl: productUrl || undefined`
- `imageFilename: imageFilename ?? undefined`
- NO `globalItemId` — deliberately omitted for standalone item
- `onSuccess` callback: call `onSuccess(item.name)` where item is the mutation result
- `onError` callback: `setError(err instanceof Error ? err.message : "Failed to save")`
**Styling:** Tailwind CSS v4 classes. Use `@/` path alias for imports. Match the visual density of AddToCollectionModal (compact, not full-page).
**Do NOT:**
- Import or reuse ItemForm directly (it's 315 lines with edit/delete logic — per D-07)
- Add `manualEntryMode` to Zustand UIStore (local state only — per research anti-patterns)
- Show a `toast.success()` on save — the success card in CatalogSearchOverlay handles that (per research anti-patterns)
</action>
<verify>
<automated>bun run lint</automated>
</verify>
<acceptance_criteria>
- File `src/client/components/ManualEntryForm.tsx` exists
- File contains `export function ManualEntryForm(`
- File contains `interface ManualEntryFormProps` with `initialName`, `onSuccess`, `onBack`
- File contains `useCreateItem()` import from `@/client/hooks/useItems`
- File contains `useCategories()` import from `@/client/hooks/useCategories`
- File contains `<CategoryPicker` usage (not a plain `<select>`)
- File contains `<ImageUpload` usage (not a raw file input)
- File contains `createItem.mutate(` call
- File does NOT contain `globalItemId` in the mutate call payload
- File does NOT contain `toast.success` or `toast("` (no self-toasting)
- File contains `ArrowLeft` import from `lucide-react`
- File contains `onBack` being called (back arrow handler)
- File contains `Math.round(Number(` for price conversion to cents
- `bun run lint` exits 0
</acceptance_criteria>
<done>ManualEntryForm component exists with all form fields per D-06, uses CategoryPicker and ImageUpload, calls useCreateItem without globalItemId, has back arrow calling onBack, and passes lint.</done>
</task>
<task type="auto">
<name>Task 2: Wire ManualEntryForm into CatalogSearchOverlay with entry points and success card</name>
<files>src/client/components/CatalogSearchOverlay.tsx</files>
<read_first>
src/client/components/CatalogSearchOverlay.tsx
src/client/components/ManualEntryForm.tsx
</read_first>
<action>
Modify `src/client/components/CatalogSearchOverlay.tsx` to add manual entry mode, entry points, and post-save success card.
**1. Add imports at top of file:**
```typescript
import { ManualEntryForm } from "./ManualEntryForm";
import { toast } from "sonner";
```
**2. Add local state (after existing useState declarations, around line 21):**
```typescript
const [manualEntryMode, setManualEntryMode] = useState(false);
const [savedItemName, setSavedItemName] = useState<string | null>(null);
```
**3. Add resets to existing reset effect (line 76-87):**
Inside the `if (!catalogSearchOpen)` block, add:
```typescript
setManualEntryMode(false);
setSavedItemName(null);
```
**4. Add handler functions (after existing toggleTag/removeTag, around line 99):**
```typescript
function handleEnterManualMode() {
setManualEntryMode(true);
}
function handleManualSuccess(itemName: string) {
setSavedItemName(itemName);
}
function handleAddAnother() {
setManualEntryMode(false);
setSavedItemName(null);
}
```
**5. Modify the header back arrow (line 136-141) to be context-sensitive (per D-04):**
Change the back arrow `onClick` from `closeCatalogSearch` to:
```typescript
onClick={manualEntryMode ? () => { setManualEntryMode(false); setSavedItemName(null); } : closeCatalogSearch}
```
Also update the context text (line 143-145) — when `manualEntryMode` is true and `savedItemName` is null, show "Manual Entry" instead of the mode text. When `savedItemName` is not null, show "Item Added".
**6. Replace the Results area content (lines 410-452) with conditional rendering (per D-03):**
The `{/* Results */}` div at line 409 should contain:
```
if manualEntryMode && savedItemName:
→ render SuccessCard (inline, see below)
else if manualEntryMode && !savedItemName:
→ render <ManualEntryForm initialName={searchInput} onSuccess={handleManualSuccess} onBack={() => { setManualEntryMode(false); setSavedItemName(null); }} />
else:
→ existing results/loading/empty rendering (unchanged)
```
When NOT in manualEntryMode, also hide the search input row and filter bar (the form doesn't need them). Actually, keep the header visible for the back arrow — just conditionally render the search input/filters. When `manualEntryMode` is true, hide the search input row and filter toggle and view toggle, but keep the back arrow + context text row.
**7. Modify EmptyState component (line 667) to accept onAddManually (per D-01, D-02):**
```typescript
function EmptyState({ hasQuery, onAddManually }: { hasQuery: boolean; onAddManually: () => void }) {
return (
<div className="flex flex-col items-center justify-center py-20 px-4">
{/* existing SVG icon unchanged */}
<p className="text-sm text-gray-500 text-center mb-3">
{hasQuery ? "No items found matching your search" : "Search the catalog to find gear"}
</p>
<button
type="button"
onClick={onAddManually}
className="text-sm text-gray-500 hover:text-gray-700 underline underline-offset-2"
>
{hasQuery ? "Can't find it? Add manually" : "Add Manually"}
</button>
</div>
);
}
```
Update the `<EmptyState` call site (line 447) to pass `onAddManually={handleEnterManualMode}`.
**8. Add persistent "Add Manually" link below results (per D-01):**
After the results grid/list (after the closing `</div>` of the grid at line 431 or list at line 444), but still inside the results conditional block (`items && items.length > 0`), add:
```typescript
<div className="flex justify-center py-6">
<button
type="button"
onClick={handleEnterManualMode}
className="text-sm text-gray-400 hover:text-gray-600 underline underline-offset-2"
>
Can't find it? Add manually
</button>
</div>
```
**9. Create inline SuccessCard (per D-09, D-10, D-11):**
Add a local component or inline JSX for the success card, rendered when `manualEntryMode && savedItemName`:
```typescript
<div className="flex flex-col items-center justify-center py-20 px-4">
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<p className="text-sm font-medium text-gray-900 mb-1">Added {savedItemName} to collection</p>
<button
type="button"
onClick={() => toast("Coming soon — catalog submissions are on the roadmap!")}
className="mt-3 text-sm text-blue-600 hover:text-blue-800 underline underline-offset-2"
>
Submit to Catalog?
</button>
<div className="flex gap-4 mt-6">
<button
type="button"
onClick={handleAddAnother}
className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
Add Another
</button>
<button
type="button"
onClick={closeCatalogSearch}
className="px-4 py-2 text-sm text-white bg-gray-900 rounded-lg hover:bg-gray-800 transition-colors"
>
Done
</button>
</div>
</div>
```
**"Submit to Catalog?" button (per D-10):** Calls `toast("Coming soon — catalog submissions are on the roadmap!")` only. No loading state, no disabled, no API call.
**Do NOT:**
- Add any state to Zustand UIStore
- Show a separate `toast.success()` when the item is saved (the success card IS the feedback)
- Make "Add Manually" only visible in collection mode — show it in both modes (per research open question 2, D-08 locks to POST /api/items regardless)
</action>
<verify>
<automated>bun run lint</automated>
</verify>
<acceptance_criteria>
- `CatalogSearchOverlay.tsx` contains `import { ManualEntryForm }` from `./ManualEntryForm`
- `CatalogSearchOverlay.tsx` contains `import { toast } from "sonner"`
- `CatalogSearchOverlay.tsx` contains `useState(false)` for `manualEntryMode`
- `CatalogSearchOverlay.tsx` contains `useState<string | null>(null)` for `savedItemName`
- `CatalogSearchOverlay.tsx` contains `setManualEntryMode(false)` inside the reset effect (the `if (!catalogSearchOpen)` block)
- `CatalogSearchOverlay.tsx` contains `setSavedItemName(null)` inside the reset effect
- `EmptyState` function signature contains `onAddManually`
- `EmptyState` renders text "Can't find it? Add manually" (per D-02)
- `EmptyState` renders text "Add Manually" (per D-02)
- `CatalogSearchOverlay.tsx` contains `<ManualEntryForm` with `initialName={searchInput}` prop
- `CatalogSearchOverlay.tsx` contains `onSuccess={handleManualSuccess}` on ManualEntryForm
- There is a persistent "Add manually" button/link rendered after search results (outside EmptyState)
- Success card contains text "Added" and "to collection"
- Success card contains "Submit to Catalog?" button text
- `toast("Coming soon` appears in the file (for Submit to Catalog button)
- Success card contains "Add Another" button text
- Success card contains "Done" button text
- "Done" button calls `closeCatalogSearch`
- "Add Another" button calls `handleAddAnother`
- Back arrow onClick is context-sensitive (checks `manualEntryMode`)
- `bun run lint` exits 0
</acceptance_criteria>
<done>CatalogSearchOverlay has "Add Manually" links in both empty state and below results, renders ManualEntryForm inline when manualEntryMode is active, shows success card with non-functional "Submit to Catalog?" button after save, and properly resets state on overlay close.</done>
</task>
</tasks>
<verification>
1. `bun run lint` passes with no errors
2. `bun test` passes (no backend changes, existing tests unaffected)
3. Manual verification: open catalog search, search for non-existent item, see "Can't find it? Add manually" link, click it, fill form, submit, see success card, click "Submit to Catalog?" and see toast, click "Add Another" to return to search, click "Done" to close overlay
</verification>
<success_criteria>
- ManualEntryForm.tsx exists as a focused, compact form using CategoryPicker and ImageUpload
- CatalogSearchOverlay renders "Add Manually" links in empty state and below results
- Clicking "Add Manually" shows the form inline, pre-populated with search query as item name
- Submitting the form creates a standalone item (no globalItemId) via useCreateItem
- Success card shows with "Submit to Catalog?" (toast only), "Add Another", and "Done"
- Back arrow returns to search results, not closes overlay
- All state resets when overlay closes
- No new Zustand store state added
- Lint and existing tests pass
</success_criteria>
<output>
After completion, create `.planning/phases/23-manual-entry-fallback/23-01-SUMMARY.md`
</output>