diff --git a/server/routes/meals.js b/server/routes/meals.js index 1c5e98e..c4aa75c 100644 --- a/server/routes/meals.js +++ b/server/routes/meals.js @@ -41,6 +41,52 @@ function weekEnd(dateStr) { return d.toISOString().slice(0, 10); } +function loadCookAssignments(mealIds) { + if (!mealIds.length) return {}; + const placeholders = mealIds.map(() => '?').join(','); + const rows = db.get().prepare(` + SELECT a.*, u.display_name AS cook_name, u.avatar_color AS cook_color + FROM planned_meal_cooks a + LEFT JOIN users u ON u.id = a.user_id + WHERE a.meal_id IN (${placeholders}) + `).all(...mealIds); + return Object.fromEntries(rows.map((row) => [row.meal_id, row])); +} + +function attachCookAssignment(meal, cookMap) { + return { + ...meal, + cook_assignment: cookMap[meal.id] || null, + }; +} + +function validateCookUserId(raw) { + if (raw === undefined) return { present: false, value: null, error: null }; + if (raw === null || raw === '') return { present: true, value: null, error: null }; + const id = Number(raw); + if (!Number.isInteger(id) || id <= 0) return { present: true, value: null, error: 'Cook user ID is invalid.' }; + const exists = db.get().prepare('SELECT id FROM users WHERE id = ?').get(id); + if (!exists) return { present: true, value: null, error: 'Cook user not found.' }; + return { present: true, value: id, error: null }; +} + +function saveCookAssignment(meal, cookUserId, sourcePlanId, createdBy) { + if (cookUserId === null) { + db.get().prepare('DELETE FROM planned_meal_cooks WHERE meal_id = ?').run(meal.id); + return; + } + db.get().prepare(` + INSERT INTO planned_meal_cooks (meal_id, user_id, planned_for_date, meal_type, source_plan_id, created_by, updated_at) + VALUES (?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ON CONFLICT(meal_id) DO UPDATE SET + user_id = excluded.user_id, + planned_for_date = excluded.planned_for_date, + meal_type = excluded.meal_type, + source_plan_id = excluded.source_plan_id, + updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') + `).run(meal.id, cookUserId, meal.date, meal.meal_type, sourcePlanId || null, createdBy); +} + // -------------------------------------------------------- // Routen - Mahlzeiten-Vorschläge (vor dynamischen Routen!) // -------------------------------------------------------- @@ -126,10 +172,11 @@ router.get('/', (req, res) => { } } - const result = meals.map((m) => ({ + const cookMap = loadCookAssignments(mealIds); + const result = meals.map((m) => attachCookAssignment({ ...m, ingredients: ingredientMap[m.id] || [], - })); + }, cookMap)); res.json({ data: result, weekStart: from, weekEnd: to }); } catch (err) { @@ -145,7 +192,7 @@ router.get('/', (req, res) => { /** * POST /api/v1/meals * Neue Mahlzeit anlegen. - * Body: { date, meal_type, title, notes?, ingredients?: [{ name, quantity? }] } + * Body: { date, meal_type, title, notes?, cook_user_id?, source_plan_id?, ingredients?: [{ name, quantity? }] } * Response: { data: Meal } */ router.post('/', (req, res) => { @@ -157,7 +204,11 @@ router.post('/', (req, res) => { const vNotes = str(req.body.notes, 'Notizen', { max: MAX_TEXT, required: false }); const vRecipeUrl = str(req.body.recipe_url, 'Rezept-URL', { max: MAX_TEXT, required: false }); const vRecipeId = num(req.body.recipe_id, 'Rezept-ID', { required: false }); - const errors = collectErrors([vDate, vType, vTitle, vNotes, vRecipeUrl, vRecipeId]); + const cookUserRaw = Object.hasOwn(req.body, 'cook_user_id') ? req.body.cook_user_id : req.body.cookUserId; + const vCookUserId = validateCookUserId(cookUserRaw); + const vSourcePlanId = str(req.body.source_plan_id ?? req.body.sourcePlanId, 'Plan-ID', { max: MAX_SHORT, required: false }); + const errors = collectErrors([vDate, vType, vTitle, vNotes, vRecipeUrl, vRecipeId, vSourcePlanId]); + if (vCookUserId.error) errors.push(vCookUserId.error); if (!req.body.meal_type) errors.push('Mahlzeit-Typ ist erforderlich.'); if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 }); @@ -185,12 +236,18 @@ router.post('/', (req, res) => { if (name) insertIng.run(mealId, name, qty, category); } - return db.get().prepare(` + const createdMeal = db.get().prepare(` SELECT m.*, u.display_name AS creator_name, u.avatar_color AS creator_color FROM meals m LEFT JOIN users u ON u.id = m.created_by WHERE m.id = ? `).get(mealId); + + if (vCookUserId.present && vCookUserId.value !== null) { + saveCookAssignment(createdMeal, vCookUserId.value, vSourcePlanId.value, req.session.userId); + } + + return createdMeal; }); // Zutaten anhängen @@ -198,7 +255,8 @@ router.post('/', (req, res) => { 'SELECT * FROM meal_ingredients WHERE meal_id = ? ORDER BY id ASC' ).all(meal.id); - res.status(201).json({ data: { ...meal, ingredients: ings } }); + const cookMap = loadCookAssignments([meal.id]); + res.status(201).json({ data: attachCookAssignment({ ...meal, ingredients: ings }, cookMap) }); } catch (err) { log.error('', err); res.status(500).json({ error: 'Interner Fehler', code: 500 }); @@ -224,7 +282,11 @@ router.put('/:id', (req, res) => { if (req.body.notes !== undefined) checks.push(str(req.body.notes, 'Notizen', { max: MAX_TEXT, required: false })); if (req.body.recipe_url !== undefined) checks.push(str(req.body.recipe_url, 'Rezept-URL', { max: MAX_TEXT, required: false })); if (req.body.recipe_id !== undefined) checks.push(num(req.body.recipe_id, 'Rezept-ID', { required: false })); - const errors = collectErrors(checks); + const cookUserRaw = Object.hasOwn(req.body, 'cook_user_id') ? req.body.cook_user_id : req.body.cookUserId; + const vCookUserId = validateCookUserId(cookUserRaw); + const vSourcePlanId = str(req.body.source_plan_id ?? req.body.sourcePlanId, 'Plan-ID', { max: MAX_SHORT, required: false }); + const errors = collectErrors([...checks, vSourcePlanId]); + if (vCookUserId.error) errors.push(vCookUserId.error); if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 }); if (req.body.recipe_id !== undefined && req.body.recipe_id !== null && req.body.recipe_id !== '') { @@ -257,11 +319,16 @@ router.put('/:id', (req, res) => { WHERE m.id = ? `).get(id); + if (vCookUserId.present) { + saveCookAssignment(updated, vCookUserId.value, vSourcePlanId.value, req.session.userId); + } + const ings = db.get().prepare( 'SELECT * FROM meal_ingredients WHERE meal_id = ? ORDER BY id ASC' ).all(id); + const cookMap = loadCookAssignments([id]); - res.json({ data: { ...updated, ingredients: ings } }); + res.json({ data: attachCookAssignment({ ...updated, ingredients: ings }, cookMap) }); } catch (err) { log.error('', err); res.status(500).json({ error: 'Interner Fehler', code: 500 });