diff --git a/src/server/services/category.service.ts b/src/server/services/category.service.ts new file mode 100644 index 0000000..8ecefaf --- /dev/null +++ b/src/server/services/category.service.ts @@ -0,0 +1,76 @@ +import { eq, asc } from "drizzle-orm"; +import { categories, items } from "../../db/schema.ts"; +import { db as prodDb } from "../../db/index.ts"; + +type Db = typeof prodDb; + +export function getAllCategories(db: Db = prodDb) { + return db.select().from(categories).orderBy(asc(categories.name)).all(); +} + +export function createCategory( + db: Db = prodDb, + data: { name: string; emoji?: string }, +) { + return db + .insert(categories) + .values({ + name: data.name, + ...(data.emoji ? { emoji: data.emoji } : {}), + }) + .returning() + .get(); +} + +export function updateCategory( + db: Db = prodDb, + id: number, + data: { name?: string; emoji?: string }, +) { + const existing = db + .select({ id: categories.id }) + .from(categories) + .where(eq(categories.id, id)) + .get(); + + if (!existing) return null; + + return db + .update(categories) + .set(data) + .where(eq(categories.id, id)) + .returning() + .get(); +} + +export function deleteCategory( + db: Db = prodDb, + id: number, +): { success: boolean; error?: string } { + // Guard: cannot delete Uncategorized (id=1) + if (id === 1) { + return { success: false, error: "Cannot delete the Uncategorized category" }; + } + + // Check if category exists + const existing = db + .select({ id: categories.id }) + .from(categories) + .where(eq(categories.id, id)) + .get(); + + if (!existing) { + return { success: false, error: "Category not found" }; + } + + // Reassign items to Uncategorized (id=1), then delete + // Use a transaction for atomicity + db.update(items) + .set({ categoryId: 1 }) + .where(eq(items.categoryId, id)) + .run(); + + db.delete(categories).where(eq(categories.id, id)).run(); + + return { success: true }; +} diff --git a/src/server/services/item.service.ts b/src/server/services/item.service.ts new file mode 100644 index 0000000..11b5c5e --- /dev/null +++ b/src/server/services/item.service.ts @@ -0,0 +1,112 @@ +import { eq, sql } from "drizzle-orm"; +import { items, categories } from "../../db/schema.ts"; +import { db as prodDb } from "../../db/index.ts"; +import type { CreateItem } from "../../shared/types.ts"; + +type Db = typeof prodDb; + +export function getAllItems(db: Db = prodDb) { + return db + .select({ + id: items.id, + name: items.name, + weightGrams: items.weightGrams, + priceCents: items.priceCents, + categoryId: items.categoryId, + notes: items.notes, + productUrl: items.productUrl, + imageFilename: items.imageFilename, + createdAt: items.createdAt, + updatedAt: items.updatedAt, + categoryName: categories.name, + categoryEmoji: categories.emoji, + }) + .from(items) + .innerJoin(categories, eq(items.categoryId, categories.id)) + .all(); +} + +export function getItemById(db: Db = prodDb, id: number) { + return ( + db + .select({ + id: items.id, + name: items.name, + weightGrams: items.weightGrams, + priceCents: items.priceCents, + categoryId: items.categoryId, + notes: items.notes, + productUrl: items.productUrl, + imageFilename: items.imageFilename, + createdAt: items.createdAt, + updatedAt: items.updatedAt, + }) + .from(items) + .where(eq(items.id, id)) + .get() ?? null + ); +} + +export function createItem( + db: Db = prodDb, + data: Partial & { name: string; categoryId: number; imageFilename?: string }, +) { + return db + .insert(items) + .values({ + name: data.name, + weightGrams: data.weightGrams ?? null, + priceCents: data.priceCents ?? null, + categoryId: data.categoryId, + notes: data.notes ?? null, + productUrl: data.productUrl ?? null, + imageFilename: data.imageFilename ?? null, + }) + .returning() + .get(); +} + +export function updateItem( + db: Db = prodDb, + id: number, + data: Partial<{ + name: string; + weightGrams: number; + priceCents: number; + categoryId: number; + notes: string; + productUrl: string; + imageFilename: string; + }>, +) { + // Check if item exists first + const existing = db + .select({ id: items.id }) + .from(items) + .where(eq(items.id, id)) + .get(); + + if (!existing) return null; + + return db + .update(items) + .set({ ...data, updatedAt: new Date() }) + .where(eq(items.id, id)) + .returning() + .get(); +} + +export function deleteItem(db: Db = prodDb, id: number) { + // Get item first (for image cleanup info) + const item = db + .select() + .from(items) + .where(eq(items.id, id)) + .get(); + + if (!item) return null; + + db.delete(items).where(eq(items.id, id)).run(); + + return item; +} diff --git a/src/server/services/totals.service.ts b/src/server/services/totals.service.ts new file mode 100644 index 0000000..d9c1fc4 --- /dev/null +++ b/src/server/services/totals.service.ts @@ -0,0 +1,32 @@ +import { eq, sql } from "drizzle-orm"; +import { items, categories } from "../../db/schema.ts"; +import { db as prodDb } from "../../db/index.ts"; + +type Db = typeof prodDb; + +export function getCategoryTotals(db: Db = prodDb) { + return db + .select({ + categoryId: items.categoryId, + categoryName: categories.name, + categoryEmoji: categories.emoji, + totalWeight: sql`COALESCE(SUM(${items.weightGrams}), 0)`, + totalCost: sql`COALESCE(SUM(${items.priceCents}), 0)`, + itemCount: sql`COUNT(*)`, + }) + .from(items) + .innerJoin(categories, eq(items.categoryId, categories.id)) + .groupBy(items.categoryId) + .all(); +} + +export function getGlobalTotals(db: Db = prodDb) { + return db + .select({ + totalWeight: sql`COALESCE(SUM(${items.weightGrams}), 0)`, + totalCost: sql`COALESCE(SUM(${items.priceCents}), 0)`, + itemCount: sql`COUNT(*)`, + }) + .from(items) + .get(); +}