---
phase: 08-search-filter-and-candidate-status
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/db/schema.ts
  - src/shared/schemas.ts
  - src/server/services/thread.service.ts
  - src/client/hooks/useThreads.ts
  - src/client/hooks/useCandidates.ts
  - src/client/components/StatusBadge.tsx
  - src/client/components/CandidateCard.tsx
  - tests/helpers/db.ts
  - tests/services/thread.service.test.ts
autonomous: true
requirements: [CAND-01, CAND-02, CAND-03]

must_haves:
  truths:
    - "Each candidate displays a status badge showing one of three statuses: researching, ordered, or arrived"
    - "User can click a status badge to open a popup menu and change the candidate's status to any of the three options"
    - "New candidates automatically have status 'researching' without the user needing to set it"
  artifacts:
    - path: "src/db/schema.ts"
      provides: "status column on threadCandidates table"
      contains: "status: text(\"status\").notNull().default(\"researching\")"
    - path: "src/shared/schemas.ts"
      provides: "candidateStatusSchema Zod enum"
      exports: ["candidateStatusSchema"]
    - path: "src/server/services/thread.service.ts"
      provides: "status field in candidate CRUD operations"
      contains: "status: threadCandidates.status"
    - path: "src/client/components/StatusBadge.tsx"
      provides: "Clickable status badge with popup menu"
      exports: ["StatusBadge"]
    - path: "src/client/components/CandidateCard.tsx"
      provides: "CandidateCard renders StatusBadge in pill row"
      contains: "StatusBadge"
    - path: "tests/helpers/db.ts"
      provides: "status column in test helper CREATE TABLE"
      contains: "status TEXT NOT NULL DEFAULT 'researching'"
  key_links:
    - from: "src/client/components/StatusBadge.tsx"
      to: "/api/threads/:id/candidates/:candidateId"
      via: "useUpdateCandidate mutation"
      pattern: "onStatusChange"
    - from: "src/server/services/thread.service.ts"
      to: "src/db/schema.ts"
      via: "threadCandidates.status in select and update"
      pattern: "threadCandidates\\.status"
    - from: "src/client/components/CandidateCard.tsx"
      to: "src/client/components/StatusBadge.tsx"
      via: "StatusBadge component in pill row"
      pattern: "<StatusBadge"
---

<objective>
Add candidate status tracking (researching/ordered/arrived) as a full vertical slice: schema migration, service/Zod updates, tests, and clickable status badge UI on CandidateCard.

Purpose: Let users track purchase progress for candidates they are evaluating in planning threads.
Output: Working status badge on each candidate card with popup menu to change status.
</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/08-search-filter-and-candidate-status/08-CONTEXT.md
@.planning/phases/08-search-filter-and-candidate-status/08-RESEARCH.md

@src/db/schema.ts
@src/shared/schemas.ts
@src/shared/types.ts
@src/server/services/thread.service.ts
@src/client/hooks/useThreads.ts
@src/client/hooks/useCandidates.ts
@src/client/components/CandidateCard.tsx
@tests/helpers/db.ts
@tests/services/thread.service.test.ts

<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->

From src/shared/types.ts:
```typescript
export type CreateCandidate = z.infer<typeof createCandidateSchema>;
export type UpdateCandidate = z.infer<typeof updateCandidateSchema>;
export type ThreadCandidate = typeof threadCandidates.$inferSelect;
```

From src/client/hooks/useCandidates.ts:
```typescript
export function useUpdateCandidate(threadId: number) {
  // mutationFn: ({ candidateId, ...data }) => apiPut(...)
  // Already accepts partial updates. Use for status changes.
}
```

From src/client/hooks/useThreads.ts:
```typescript
interface CandidateWithCategory {
  id: number; threadId: 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;
  // status field NOT YET present -- Task 1 adds it
}
```

From src/client/components/CandidateCard.tsx:
```typescript
interface CandidateCardProps {
  id: number; name: string; weightGrams: number | null;
  priceCents: number | null; categoryName: string;
  categoryIcon: string; imageFilename: string | null;
  productUrl?: string | null; threadId: number; isActive: boolean;
  // status prop NOT YET present -- Task 2 adds it
}
```

From src/client/lib/iconData.tsx:
```typescript
export function LucideIcon({ name, size, className }: {
  name: string; size?: number; className?: string;
}): JSX.Element;
// Valid icon names for status: "search", "truck", "check"
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Add status column and update backend + tests</name>
  <files>src/db/schema.ts, src/shared/schemas.ts, src/server/services/thread.service.ts, src/client/hooks/useThreads.ts, src/client/hooks/useCandidates.ts, tests/helpers/db.ts, tests/services/thread.service.test.ts</files>
  <behavior>
    - Test: createCandidate without status returns a candidate with status "researching"
    - Test: createCandidate with status "ordered" returns a candidate with status "ordered"
    - Test: updateCandidate can change status from "researching" to "ordered"
    - Test: updateCandidate can change status from "ordered" to "arrived"
    - Test: getThreadWithCandidates includes status field on each candidate
  </behavior>
  <action>
    1. **Schema migration** -- Add status column to `threadCandidates` in `src/db/schema.ts`:
       ```typescript
       status: text("status").notNull().default("researching"),
       ```
       Then run `bun run db:generate && bun run db:push` to apply.

    2. **Zod schemas** -- In `src/shared/schemas.ts`, add:
       ```typescript
       export const candidateStatusSchema = z.enum(["researching", "ordered", "arrived"]);
       ```
       Add `status: candidateStatusSchema.optional()` to `createCandidateSchema`. Since `updateCandidateSchema = createCandidateSchema.partial()`, it automatically includes status as optional.

    3. **Service updates** -- In `src/server/services/thread.service.ts`:
       - In `getThreadWithCandidates`, add `status: threadCandidates.status` to the select object (between `imageFilename` and `createdAt`).
       - In `createCandidate`, add `status: data.status ?? "researching"` to the values object.
       - In `updateCandidate`, add `status` to the data type: `status: "researching" | "ordered" | "arrived"`.

    4. **Client type updates** -- In `src/client/hooks/useThreads.ts`, add `status: "researching" | "ordered" | "arrived"` to `CandidateWithCategory` interface. In `src/client/hooks/useCandidates.ts`, add `status?: "researching" | "ordered" | "arrived"` to `CandidateResponse` interface.

    5. **Test helper** -- In `tests/helpers/db.ts`, add `status TEXT NOT NULL DEFAULT 'researching'` to the `thread_candidates` CREATE TABLE statement (after `image_filename TEXT` line).

    6. **Service tests** -- In `tests/services/thread.service.test.ts`, add a describe block "candidate status" with the tests from the behavior section above.
  </action>
  <verify>
    <automated>cd /home/jlmak/Projects/jlmak/GearBox && bun test tests/services/thread.service.test.ts</automated>
  </verify>
  <done>Status column exists in schema, migration applied, all CRUD operations handle status field, all tests pass including new status tests.</done>
</task>

<task type="auto">
  <name>Task 2: Create StatusBadge component and wire into CandidateCard</name>
  <files>src/client/components/StatusBadge.tsx, src/client/components/CandidateCard.tsx</files>
  <action>
    1. **Create `src/client/components/StatusBadge.tsx`** -- A clickable pill badge with popup menu:
       - Props: `status: "researching" | "ordered" | "arrived"`, `onStatusChange: (status: "researching" | "ordered" | "arrived") => void`
       - Status config map:
         ```typescript
         const STATUS_CONFIG = {
           researching: { icon: "search", label: "Researching" },
           ordered: { icon: "truck", label: "Ordered" },
           arrived: { icon: "check", label: "Arrived" },
         } as const;
         ```
       - Render as a pill button (muted gray tones per user decision -- NOT semantic colors):
         - Use `bg-gray-100 text-gray-600` styling, similar neutral tone to the category pill
         - Show `LucideIcon` (size 14) + text label
       - On click: call `e.stopPropagation()` (prevent card click propagation per pitfall #3), toggle popup menu open/closed
       - Popup menu: `position: absolute` below the badge, `right-0`, with 3 options (each showing icon + label). Use a `containerRef` + `useEffect` mousedown listener for click-outside dismiss (same pattern as `CategoryPicker`). Pressing Escape also closes the menu.
       - When an option is clicked: call `onStatusChange(selectedStatus)`, close the menu.
       - Show a subtle checkmark or different background on the currently active status in the menu.

    2. **Update `src/client/components/CandidateCard.tsx`**:
       - Add `status: "researching" | "ordered" | "arrived"` and `onStatusChange: (status: "researching" | "ordered" | "arrived") => void` to `CandidateCardProps`.
       - Import `StatusBadge` from `./StatusBadge`.
       - Add `<StatusBadge status={status} onStatusChange={onStatusChange} />` to the pill row (the `flex flex-wrap gap-1.5 mb-3` div), after the category pill.

    3. **Update thread detail page caller** -- Find where `CandidateCard` is rendered (in the thread detail route). Add the `status` and `onStatusChange` props. For `onStatusChange`, use the existing `useUpdateCandidate` hook: `updateCandidate.mutate({ candidateId: candidate.id, status: newStatus })`.
  </action>
  <verify>
    <automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run lint && bun test</automated>
  </verify>
  <done>Each candidate card shows a gray status badge (icon + label) in the pill row. Clicking the badge opens a popup menu with all three status options. Selecting a status updates it via the API and the badge reflects the new status. New candidates show "Researching" by default.</done>
</task>

</tasks>

<verification>
1. `bun test` -- all existing and new tests pass
2. `bun run lint` -- no lint errors
3. Start dev server (`bun run dev:server` + `bun run dev:client`), navigate to a thread detail page, verify:
   - Each candidate shows a gray "Researching" badge in the pill row
   - Clicking the badge opens a popup menu with Researching, Ordered, Arrived options
   - Selecting a different status updates the badge immediately
   - Refreshing the page shows the persisted status
</verification>

<success_criteria>
- Status column exists on thread_candidates table with default "researching"
- All candidate CRUD operations handle the status field
- StatusBadge component renders in CandidateCard pill row with muted gray styling
- Clicking badge opens popup menu, selecting an option changes status via API
- New candidates show "researching" status by default
- All tests pass including 5 new status-specific tests
</success_criteria>

<output>
After completion, create `.planning/phases/08-search-filter-and-candidate-status/08-01-SUMMARY.md`
</output>
