import type Database from 'better-sqlite3'; // Fallback when a recipe has no servings_default set — matches the default // used by RecipeEditor's "new recipe" template. const DEFAULT_SERVINGS = 4; export type ShoppingCartRecipe = { recipe_id: number; title: string; image_path: string | null; servings: number; servings_default: number; }; export type ShoppingListRow = { name_key: string; unit_key: string; display_name: string; display_unit: string | null; total_quantity: number | null; from_recipes: string; checked: 0 | 1; }; export type ShoppingListSnapshot = { recipes: ShoppingCartRecipe[]; rows: ShoppingListRow[]; uncheckedCount: number; }; export function addRecipeToCart( db: Database.Database, recipeId: number, profileId: number | null, servings?: number ): void { const row = db .prepare('SELECT servings_default FROM recipe WHERE id = ?') .get(recipeId) as { servings_default: number | null } | undefined; const resolved = servings ?? row?.servings_default ?? DEFAULT_SERVINGS; // ON CONFLICT updates only servings — added_by_profile_id stays with the // first profile that added the recipe (household cart, audit trail). db.prepare( `INSERT INTO shopping_cart_recipe (recipe_id, servings, added_by_profile_id) VALUES (?, ?, ?) ON CONFLICT(recipe_id) DO UPDATE SET servings = excluded.servings` ).run(recipeId, resolved, profileId); } export function removeRecipeFromCart( db: Database.Database, recipeId: number ): void { db.prepare('DELETE FROM shopping_cart_recipe WHERE recipe_id = ?').run(recipeId); } export function setCartServings( db: Database.Database, recipeId: number, servings: number ): void { if (!Number.isInteger(servings) || servings <= 0) { throw new Error(`Invalid servings: ${servings}`); } db.prepare( 'UPDATE shopping_cart_recipe SET servings = ? WHERE recipe_id = ?' ).run(servings, recipeId); } export function listShoppingList( db: Database.Database ): ShoppingListSnapshot { const recipes = db .prepare( `SELECT cr.recipe_id, r.title, r.image_path, cr.servings, COALESCE(r.servings_default, cr.servings) AS servings_default FROM shopping_cart_recipe cr JOIN recipe r ON r.id = cr.recipe_id ORDER BY cr.added_at ASC` ) .all() as ShoppingCartRecipe[]; const rows = db .prepare( `SELECT LOWER(TRIM(i.name)) AS name_key, LOWER(TRIM(COALESCE(i.unit, ''))) AS unit_key, MIN(i.name) AS display_name, MIN(i.unit) AS display_unit, SUM(i.quantity * cr.servings * 1.0 / NULLIF(COALESCE(r.servings_default, cr.servings), 0)) AS total_quantity, GROUP_CONCAT(DISTINCT r.title) AS from_recipes, EXISTS( SELECT 1 FROM shopping_cart_check c WHERE c.name_key = LOWER(TRIM(i.name)) AND c.unit_key = LOWER(TRIM(COALESCE(i.unit, ''))) ) AS checked FROM shopping_cart_recipe cr JOIN recipe r ON r.id = cr.recipe_id JOIN ingredient i ON i.recipe_id = r.id GROUP BY name_key, unit_key ORDER BY checked ASC, display_name COLLATE NOCASE` ) .all() as ShoppingListRow[]; const uncheckedCount = rows.reduce((n, r) => n + (r.checked ? 0 : 1), 0); return { recipes, rows, uncheckedCount }; } export function toggleCheck( db: Database.Database, nameKey: string, unitKey: string, checked: boolean ): void { if (checked) { db.prepare( `INSERT INTO shopping_cart_check (name_key, unit_key) VALUES (?, ?) ON CONFLICT(name_key, unit_key) DO NOTHING` ).run(nameKey, unitKey); } else { db.prepare( 'DELETE FROM shopping_cart_check WHERE name_key = ? AND unit_key = ?' ).run(nameKey, unitKey); } } export function clearCheckedItems(db: Database.Database): void { const tx = db.transaction(() => { // Alle aggregierten Zeilen mit checked-Status holen, pro recipe_id gruppieren // und Rezepte finden, deren Zeilen ALLE abgehakt sind. const allRows = db .prepare( `SELECT cr.recipe_id, LOWER(TRIM(i.name)) AS name_key, LOWER(TRIM(COALESCE(i.unit, ''))) AS unit_key, EXISTS( SELECT 1 FROM shopping_cart_check c WHERE c.name_key = LOWER(TRIM(i.name)) AND c.unit_key = LOWER(TRIM(COALESCE(i.unit, ''))) ) AS checked FROM shopping_cart_recipe cr JOIN ingredient i ON i.recipe_id = cr.recipe_id` ) .all() as { recipe_id: number; name_key: string; unit_key: string; checked: 0 | 1 }[]; const perRecipe = new Map(); for (const r of allRows) { const e = perRecipe.get(r.recipe_id) ?? { total: 0, checked: 0 }; e.total += 1; e.checked += r.checked; perRecipe.set(r.recipe_id, e); } const toRemove: number[] = []; for (const [id, e] of perRecipe) { if (e.total > 0 && e.total === e.checked) toRemove.push(id); } for (const id of toRemove) { db.prepare('DELETE FROM shopping_cart_recipe WHERE recipe_id = ?').run(id); } // Orphan-Checks raeumen: alle Check-Keys, die jetzt in KEINEM Cart-Rezept // mehr vorkommen. const activeKeys = db .prepare( `SELECT DISTINCT LOWER(TRIM(i.name)) AS name_key, LOWER(TRIM(COALESCE(i.unit, ''))) AS unit_key FROM shopping_cart_recipe cr JOIN ingredient i ON i.recipe_id = cr.recipe_id` ) .all() as { name_key: string; unit_key: string }[]; const activeSet = new Set(activeKeys.map((k) => `${k.name_key} ${k.unit_key}`)); const allChecks = db .prepare('SELECT name_key, unit_key FROM shopping_cart_check') .all() as { name_key: string; unit_key: string }[]; const del = db.prepare( 'DELETE FROM shopping_cart_check WHERE name_key = ? AND unit_key = ?' ); for (const c of allChecks) { if (!activeSet.has(`${c.name_key} ${c.unit_key}`)) { del.run(c.name_key, c.unit_key); } } }); tx(); } export function clearCart(db: Database.Database): void { const tx = db.transaction(() => { db.prepare('DELETE FROM shopping_cart_recipe').run(); db.prepare('DELETE FROM shopping_cart_check').run(); }); tx(); }