From ca1c2a2e5783e2fc2605e8c159ae39c2ca5f8dd9 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 16 Mar 2026 14:09:18 +0100 Subject: [PATCH] feat(08-01): add status column to threadCandidates and wire through backend - Schema: status TEXT NOT NULL DEFAULT 'researching' on thread_candidates - Zod: candidateStatusSchema enum (researching/ordered/arrived) added to createCandidateSchema - Service: getThreadWithCandidates selects status, createCandidate sets status, updateCandidate accepts status - Client hooks: CandidateWithCategory and CandidateResponse types include status field - Migration generated and applied Co-Authored-By: Claude Opus 4.6 --- drizzle/0002_broken_roughhouse.sql | 1 + drizzle/meta/0002_snapshot.json | 475 ++++++++++++++++++++++++++ drizzle/meta/_journal.json | 45 +-- src/client/hooks/useCandidates.ts | 1 + src/client/hooks/useThreads.ts | 1 + src/db/schema.ts | 1 + src/server/services/thread.service.ts | 3 + src/shared/schemas.ts | 4 + 8 files changed, 512 insertions(+), 19 deletions(-) create mode 100644 drizzle/0002_broken_roughhouse.sql create mode 100644 drizzle/meta/0002_snapshot.json diff --git a/drizzle/0002_broken_roughhouse.sql b/drizzle/0002_broken_roughhouse.sql new file mode 100644 index 0000000..8c6e3d2 --- /dev/null +++ b/drizzle/0002_broken_roughhouse.sql @@ -0,0 +1 @@ +ALTER TABLE `thread_candidates` ADD `status` text DEFAULT 'researching' NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..8aa5411 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,475 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "ad8099fa-5c3f-4918-9e21-a259cae77d4f", + "prevId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "tables": { + "categories": { + "name": "categories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'package'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "categories_name_unique": { + "name": "categories_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "items": { + "name": "items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "weight_grams": { + "name": "weight_grams", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "product_url": { + "name": "product_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_filename": { + "name": "image_filename", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "items_category_id_categories_id_fk": { + "name": "items_category_id_categories_id_fk", + "tableFrom": "items", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "setup_items": { + "name": "setup_items", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "setup_id": { + "name": "setup_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "setup_items_setup_id_setups_id_fk": { + "name": "setup_items_setup_id_setups_id_fk", + "tableFrom": "setup_items", + "tableTo": "setups", + "columnsFrom": [ + "setup_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "setup_items_item_id_items_id_fk": { + "name": "setup_items_item_id_items_id_fk", + "tableFrom": "setup_items", + "tableTo": "items", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "setups": { + "name": "setups", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "thread_candidates": { + "name": "thread_candidates", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "thread_id": { + "name": "thread_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "weight_grams": { + "name": "weight_grams", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "price_cents": { + "name": "price_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "product_url": { + "name": "product_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_filename": { + "name": "image_filename", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'researching'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "thread_candidates_thread_id_threads_id_fk": { + "name": "thread_candidates_thread_id_threads_id_fk", + "tableFrom": "thread_candidates", + "tableTo": "threads", + "columnsFrom": [ + "thread_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "thread_candidates_category_id_categories_id_fk": { + "name": "thread_candidates_category_id_categories_id_fk", + "tableFrom": "thread_candidates", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "threads": { + "name": "threads", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "resolved_candidate_id": { + "name": "resolved_candidate_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "threads_category_id_categories_id_fk": { + "name": "threads_category_id_categories_id_fk", + "tableFrom": "threads", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 69a6011..bb9b01d 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -1,20 +1,27 @@ { - "version": "7", - "dialect": "sqlite", - "entries": [ - { - "idx": 0, - "version": "6", - "when": 1773589489626, - "tag": "0000_bitter_luckman", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1773593102000, - "tag": "0001_rename_emoji_to_icon", - "breakpoints": true - } - ] -} + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1773589489626, + "tag": "0000_bitter_luckman", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1773593102000, + "tag": "0001_rename_emoji_to_icon", + "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1773666521689, + "tag": "0002_broken_roughhouse", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/src/client/hooks/useCandidates.ts b/src/client/hooks/useCandidates.ts index 24f4c4b..ef306e9 100644 --- a/src/client/hooks/useCandidates.ts +++ b/src/client/hooks/useCandidates.ts @@ -12,6 +12,7 @@ interface CandidateResponse { notes: string | null; productUrl: string | null; imageFilename: string | null; + status: "researching" | "ordered" | "arrived"; createdAt: string; updatedAt: string; } diff --git a/src/client/hooks/useThreads.ts b/src/client/hooks/useThreads.ts index e7222d9..b1de598 100644 --- a/src/client/hooks/useThreads.ts +++ b/src/client/hooks/useThreads.ts @@ -26,6 +26,7 @@ interface CandidateWithCategory { notes: string | null; productUrl: string | null; imageFilename: string | null; + status: "researching" | "ordered" | "arrived"; createdAt: string; updatedAt: string; categoryName: string; diff --git a/src/db/schema.ts b/src/db/schema.ts index f1f7b6c..3fc6804 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -58,6 +58,7 @@ export const threadCandidates = sqliteTable("thread_candidates", { notes: text("notes"), productUrl: text("product_url"), imageFilename: text("image_filename"), + status: text("status").notNull().default("researching"), createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .$defaultFn(() => new Date()), diff --git a/src/server/services/thread.service.ts b/src/server/services/thread.service.ts index 4baafda..2c66232 100644 --- a/src/server/services/thread.service.ts +++ b/src/server/services/thread.service.ts @@ -72,6 +72,7 @@ export function getThreadWithCandidates(db: Db = prodDb, threadId: number) { notes: threadCandidates.notes, productUrl: threadCandidates.productUrl, imageFilename: threadCandidates.imageFilename, + status: threadCandidates.status, createdAt: threadCandidates.createdAt, updatedAt: threadCandidates.updatedAt, categoryName: categories.name, @@ -149,6 +150,7 @@ export function createCandidate( notes: data.notes ?? null, productUrl: data.productUrl ?? null, imageFilename: data.imageFilename ?? null, + status: data.status ?? "researching", }) .returning() .get(); @@ -165,6 +167,7 @@ export function updateCandidate( notes: string; productUrl: string; imageFilename: string; + status: "researching" | "ordered" | "arrived"; }>, ) { const existing = db diff --git a/src/shared/schemas.ts b/src/shared/schemas.ts index a63b520..2c1bba6 100644 --- a/src/shared/schemas.ts +++ b/src/shared/schemas.ts @@ -36,6 +36,9 @@ export const updateThreadSchema = z.object({ categoryId: z.number().int().positive().optional(), }); +// Candidate status +export const candidateStatusSchema = z.enum(["researching", "ordered", "arrived"]); + // Candidate schemas (same fields as items) export const createCandidateSchema = z.object({ name: z.string().min(1, "Name is required"), @@ -45,6 +48,7 @@ export const createCandidateSchema = z.object({ notes: z.string().optional(), productUrl: z.string().url().optional().or(z.literal("")), imageFilename: z.string().optional(), + status: candidateStatusSchema.optional(), }); export const updateCandidateSchema = createCandidateSchema.partial();