feat: expose meal cook assignments on meals
This commit is contained in:
+75
-8
@@ -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 });
|
||||||
|
|||||||
Reference in New Issue
Block a user