Files
PantryHub/services/MenuService.js
T

158 lines
5.6 KiB
JavaScript

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, householdId) {
const existingNames = new Set(
ProductRepository.getAll(householdId).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(householdId);
const productsByName = new Map(
products.map(product => [normalizeName(product.name), product])
);
const insertProduct = db.prepare(`
INSERT INTO products (name, category_id, barcode, household_id) 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, householdId);
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();