Files
GearBox/src/server/services/item.service.ts
Jean-Luc Makiola 581872b534 fix(F-07): add crop/color fields to updateItem service type
The updateItem function's TypeScript type was missing dominantColor,
cropZoom, cropX, and cropY fields, causing crop settings to silently
fail to save despite the Zod schema and DB schema supporting them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 22:33:28 +02:00

222 lines
6.2 KiB
TypeScript

import { and, eq, sql } from "drizzle-orm";
import type { db as prodDb } from "../../db/index.ts";
import { categories, globalItems, items } from "../../db/schema.ts";
import type { CreateItem } from "../../shared/types.ts";
type Db = typeof prodDb;
export async function getAllItems(db: Db, userId: number) {
return db
.select({
id: items.id,
name: sql<string>`COALESCE(
CASE WHEN ${items.globalItemId} IS NOT NULL
THEN ${globalItems.brand} || ' ' || ${globalItems.model}
ELSE ${items.name}
END,
${items.name}
)`.as("name"),
weightGrams: sql<number | null>`COALESCE(
CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.weightGrams} ELSE NULL END,
${items.weightGrams}
)`.as("weight_grams"),
priceCents: sql<number | null>`COALESCE(
CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.priceCents} ELSE NULL END,
${items.priceCents}
)`.as("price_cents"),
purchasePriceCents: items.purchasePriceCents,
quantity: items.quantity,
categoryId: items.categoryId,
notes: items.notes,
productUrl: items.productUrl,
imageFilename: sql<string | null>`COALESCE(
${items.imageFilename},
CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.imageUrl} ELSE NULL END
)`.as("image_filename"),
imageSourceUrl: items.imageSourceUrl,
globalItemId: items.globalItemId,
brand: sql<
string | null
>`COALESCE(${globalItems.brand}, ${items.brand})`.as("brand"),
createdAt: items.createdAt,
updatedAt: items.updatedAt,
categoryName: categories.name,
categoryIcon: categories.icon,
})
.from(items)
.innerJoin(categories, eq(items.categoryId, categories.id))
.leftJoin(globalItems, eq(items.globalItemId, globalItems.id))
.where(eq(items.userId, userId));
}
export async function getItemById(db: Db, userId: number, id: number) {
const [row] = await db
.select({
id: items.id,
name: sql<string>`COALESCE(
CASE WHEN ${items.globalItemId} IS NOT NULL
THEN ${globalItems.brand} || ' ' || ${globalItems.model}
ELSE ${items.name}
END,
${items.name}
)`.as("name"),
weightGrams: sql<number | null>`COALESCE(
CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.weightGrams} ELSE NULL END,
${items.weightGrams}
)`.as("weight_grams"),
priceCents: sql<number | null>`COALESCE(
CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.priceCents} ELSE NULL END,
${items.priceCents}
)`.as("price_cents"),
purchasePriceCents: items.purchasePriceCents,
quantity: items.quantity,
categoryId: items.categoryId,
notes: items.notes,
productUrl: items.productUrl,
imageFilename: sql<string | null>`COALESCE(
${items.imageFilename},
CASE WHEN ${items.globalItemId} IS NOT NULL THEN ${globalItems.imageUrl} ELSE NULL END
)`.as("image_filename"),
imageSourceUrl: items.imageSourceUrl,
globalItemId: items.globalItemId,
brand: sql<
string | null
>`COALESCE(${globalItems.brand}, ${items.brand})`.as("brand"),
createdAt: items.createdAt,
updatedAt: items.updatedAt,
categoryName: categories.name,
categoryIcon: categories.icon,
})
.from(items)
.innerJoin(categories, eq(items.categoryId, categories.id))
.leftJoin(globalItems, eq(items.globalItemId, globalItems.id))
.where(and(eq(items.id, id), eq(items.userId, userId)));
return row ?? null;
}
export async function createItem(
db: Db,
userId: number,
data: Partial<CreateItem> & {
name: string;
categoryId: number;
imageFilename?: string;
},
) {
// For reference items, look up global item for fallback name (items.name is NOT NULL)
let name = data.name;
if (data.globalItemId) {
const [gi] = await db
.select({ brand: globalItems.brand, model: globalItems.model })
.from(globalItems)
.where(eq(globalItems.id, data.globalItemId));
if (gi) {
name = `${gi.brand} ${gi.model}`;
}
}
const [row] = await db
.insert(items)
.values({
name,
weightGrams: data.weightGrams ?? null,
priceCents: data.priceCents ?? null,
quantity: data.quantity ?? 1,
categoryId: data.categoryId,
userId,
notes: data.notes ?? null,
productUrl: data.productUrl ?? null,
imageFilename: data.imageFilename ?? null,
imageSourceUrl: data.imageSourceUrl ?? null,
globalItemId: data.globalItemId ?? null,
purchasePriceCents: data.purchasePriceCents ?? null,
})
.returning();
return row;
}
export async function updateItem(
db: Db,
userId: number,
id: number,
data: Partial<{
name: string;
weightGrams: number;
priceCents: number;
quantity: number;
categoryId: number;
notes: string;
productUrl: string;
imageFilename: string;
imageSourceUrl: string;
globalItemId: number;
purchasePriceCents: number;
brand: string;
dominantColor: string | null;
cropZoom: number | null;
cropX: number | null;
cropY: number | null;
}>,
) {
// Check if item exists and belongs to user
const [existing] = await db
.select({ id: items.id })
.from(items)
.where(and(eq(items.id, id), eq(items.userId, userId)));
if (!existing) return null;
const [row] = await db
.update(items)
.set({ ...data, updatedAt: new Date() })
.where(and(eq(items.id, id), eq(items.userId, userId)))
.returning();
return row;
}
export async function duplicateItem(db: Db, userId: number, id: number) {
const [source] = await db
.select()
.from(items)
.where(and(eq(items.id, id), eq(items.userId, userId)));
if (!source) return null;
const [row] = await db
.insert(items)
.values({
name: `${source.name} (copy)`,
weightGrams: source.weightGrams,
priceCents: source.priceCents,
categoryId: source.categoryId,
userId,
notes: source.notes,
productUrl: source.productUrl,
imageFilename: source.imageFilename,
imageSourceUrl: source.imageSourceUrl,
quantity: source.quantity,
globalItemId: source.globalItemId,
purchasePriceCents: source.purchasePriceCents,
})
.returning();
return row;
}
export async function deleteItem(db: Db, userId: number, id: number) {
// Get item first (for image cleanup info)
const [item] = await db
.select()
.from(items)
.where(and(eq(items.id, id), eq(items.userId, userId)));
if (!item) return null;
await db.delete(items).where(and(eq(items.id, id), eq(items.userId, userId)));
return item;
}