const db = require("../config/database"); const ProductRepository = require("../repositories/ProductRepository"); function normalizeName(name) { return name.trim().toLowerCase().replace(/\s+/g, " "); } class MenuService { async generate(preferences) { const apiKey = process.env.OPENAI_API_KEY; if (!apiKey) { throw new Error("OPENAI_API_KEY ontbreekt in de configuratie."); } const response = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` }, body: JSON.stringify({ model: process.env.OPENAI_MODEL || "gpt-4o-mini", response_format: { type: "json_object" }, messages: [ { role: "system", content: "Je maakt praktische weekmenu's. Geef uitsluitend geldige JSON terug met precies de structuur {days:[{day,recipe,ingredients:[{name,amount,unit}]}]}. Gebruik zeven dagen, Nederlandse productnamen, positieve numerieke hoeveelheden en korte recepten." }, { role: "user", content: `Maak een weekmenu voor ${preferences.people} personen. Dieet: ${preferences.diet || "geen specifiek dieet"}. Niet gewenst: ${preferences.exclude || "niets"}. Budget: ${preferences.budget || "geen voorkeur"}.` } ] }) }); if (!response.ok) { let providerMessage = "onbekende fout"; try { const errorPayload = await response.json(); providerMessage = errorPayload.error?.message || providerMessage; } catch (error) { providerMessage = `HTTP ${response.status}`; } throw new Error(`AI-service fout (${response.status}): ${providerMessage}`); } const payload = await response.json(); if (!payload.choices?.[0]?.message?.content) { throw new Error("De AI-service gaf geen menu-inhoud terug."); } let menu; try { menu = JSON.parse(payload.choices?.[0]?.message?.content || "{}"); } catch (error) { throw new Error("De AI-service gaf geen geldig menu terug."); } if (!Array.isArray(menu.days) || menu.days.length === 0) { throw new Error("De AI-service gaf een leeg menu terug."); } menu.days = menu.days.map(day => ({ day: String(day.day || "Dag"), recipe: String(day.recipe || "Onbekend recept"), ingredients: Array.isArray(day.ingredients) ? day.ingredients .filter(item => item && String(item.name || "").trim()) .map(item => ({ name: String(item.name).trim(), amount: Number.isFinite(Number(item.amount)) ? Number(item.amount) : 1, unit: String(item.unit || "") })) : [] })); return menu; } getNewProducts(menu) { const existingNames = new Set( ProductRepository.getAll().map(product => normalizeName(product.name)) ); const seen = new Set(); const newProducts = []; for (const day of menu.days) { for (const ingredient of day.ingredients) { const key = normalizeName(ingredient.name); if (!existingNames.has(key) && !seen.has(key)) { seen.add(key); newProducts.push(ingredient.name); } } } return newProducts; } addToShoppingList(householdId, name, menu) { const products = ProductRepository.getAll(); const productsByName = new Map( products.map(product => [normalizeName(product.name), product]) ); const insertProduct = db.prepare(` INSERT INTO products (name, category_id, barcode) VALUES (?, NULL, NULL) `); const insertList = db.prepare(` INSERT INTO shopping_lists (household_id, name) VALUES (?, ?) `); const insertItem = db.prepare(` INSERT INTO shopping_list_items (list_id, product_id, amount, sort_order) VALUES (?, ?, ?, ?) `); const create = db.transaction(() => { const listResult = insertList.run(householdId, name); const listId = listResult.lastInsertRowid; let sortOrder = 0; const added = new Set(); for (const day of menu.days) { for (const ingredient of day.ingredients) { const key = normalizeName(ingredient.name); if (added.has(key)) continue; let product = productsByName.get(key); if (!product) { const result = insertProduct.run(ingredient.name); product = { id: result.lastInsertRowid, name: ingredient.name }; productsByName.set(key, product); } const amount = Number(ingredient.amount); insertItem.run(listId, product.id, amount > 0 ? amount : 1, sortOrder); added.add(key); sortOrder += 1; } } return listId; }); return create(); } } module.exports = new MenuService();