feat: expose meal cook assignments on meals

This commit is contained in:
OpenClaw Bot
2026-05-11 23:11:55 +02:00
parent 0b6603b092
commit 5099155c61
+75 -8
View File
@@ -41,6 +41,52 @@ function weekEnd(dateStr) {
return d.toISOString().slice(0, 10); 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!) // 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, ...m,
ingredients: ingredientMap[m.id] || [], ingredients: ingredientMap[m.id] || [],
})); }, cookMap));
res.json({ data: result, weekStart: from, weekEnd: to }); res.json({ data: result, weekStart: from, weekEnd: to });
} catch (err) { } catch (err) {
@@ -145,7 +192,7 @@ router.get('/', (req, res) => {
/** /**
* POST /api/v1/meals * POST /api/v1/meals
* Neue Mahlzeit anlegen. * 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 } * Response: { data: Meal }
*/ */
router.post('/', (req, res) => { 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 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 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 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 (!req.body.meal_type) errors.push('Mahlzeit-Typ ist erforderlich.');
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 }); 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); 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 SELECT m.*, u.display_name AS creator_name, u.avatar_color AS creator_color
FROM meals m FROM meals m
LEFT JOIN users u ON u.id = m.created_by LEFT JOIN users u ON u.id = m.created_by
WHERE m.id = ? WHERE m.id = ?
`).get(mealId); `).get(mealId);
if (vCookUserId.present && vCookUserId.value !== null) {
saveCookAssignment(createdMeal, vCookUserId.value, vSourcePlanId.value, req.session.userId);
}
return createdMeal;
}); });
// Zutaten anhängen // Zutaten anhängen
@@ -198,7 +255,8 @@ router.post('/', (req, res) => {
'SELECT * FROM meal_ingredients WHERE meal_id = ? ORDER BY id ASC' 'SELECT * FROM meal_ingredients WHERE meal_id = ? ORDER BY id ASC'
).all(meal.id); ).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) { } catch (err) {
log.error('', err); log.error('', err);
res.status(500).json({ error: 'Interner Fehler', code: 500 }); 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.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_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 })); 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 (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 !== '') { 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 = ? WHERE m.id = ?
`).get(id); `).get(id);
if (vCookUserId.present) {
saveCookAssignment(updated, vCookUserId.value, vSourcePlanId.value, req.session.userId);
}
const ings = db.get().prepare( const ings = db.get().prepare(
'SELECT * FROM meal_ingredients WHERE meal_id = ? ORDER BY id ASC' 'SELECT * FROM meal_ingredients WHERE meal_id = ? ORDER BY id ASC'
).all(id); ).all(id);
const cookMap = loadCookAssignments([id]);
res.json({ data: { ...updated, ingredients: ings } }); res.json({ data: attachCookAssignment({ ...updated, ingredients: ings }, cookMap) });
} catch (err) { } catch (err) {
log.error('', err); log.error('', err);
res.status(500).json({ error: 'Interner Fehler', code: 500 }); res.status(500).json({ error: 'Interner Fehler', code: 500 });