158 lines
5.6 KiB
JavaScript
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();
|