feat: structure meal planning taxonomy and favorites

This commit is contained in:
OpenClaw Bot
2026-05-12 17:15:31 +02:00
parent cef366cce4
commit 58a76ee02d
9 changed files with 442 additions and 20 deletions
+49 -5
View File
@@ -15,6 +15,9 @@ const log = createLogger('Meals');
const router = express.Router();
const VALID_MEAL_TYPES = ['breakfast', 'lunch', 'dinner', 'snack'];
const VALID_MEAL_CATEGORIES = ['meat', 'fish', 'pasta', 'rice', 'vegetarian', 'soup', 'leftovers', 'cozy', 'breakfast', 'snack', 'other'];
const VALID_PROTEINS = ['mixed', 'chicken', 'beef', 'pork', 'fish', 'vegetarian', 'none', 'other'];
const VALID_STYLES = ['family', 'quick', 'cozy', 'grill', 'vegetarian', 'kids', 'leftovers', 'other'];
// --------------------------------------------------------
// Hilfsfunktionen
@@ -75,6 +78,21 @@ function currentUserId(req) {
return req.authUserId ?? req.session?.userId ?? null;
}
function normalizeEnum(value, allowed, fallback = null) {
const normalized = String(value || '').trim().toLowerCase();
return allowed.includes(normalized) ? normalized : fallback;
}
function validateLeftoverMealId(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: 'Leftover source meal ID is invalid.' };
const exists = db.get().prepare('SELECT id FROM meals WHERE id = ?').get(id);
if (!exists) return { present: true, value: null, error: 'Leftover source meal not found.' };
return { present: true, value: id, error: null };
}
function tableColumns(table) {
return new Set(db.get().prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name));
}
@@ -224,7 +242,7 @@ router.get('/', (req, res) => {
/**
* POST /api/v1/meals
* Neue Mahlzeit anlegen.
* Body: { date, meal_type, title, notes?, cook_user_id?, source_plan_id?, ingredients?: [{ name, quantity? }] }
* Body: { date, meal_type, title, notes?, meal_category?, protein?, style?, leftover_from_meal_id?, cook_user_id?, source_plan_id?, ingredients?: [{ name, quantity? }] }
* Response: { data: Meal }
*/
router.post('/', (req, res) => {
@@ -238,12 +256,19 @@ router.post('/', (req, res) => {
const vRecipeId = num(req.body.recipe_id, 'Rezept-ID', { required: false });
const cookUserRaw = Object.hasOwn(req.body, 'cook_user_id') ? req.body.cook_user_id : req.body.cookUserId;
const vCookUserId = validateCookUserId(cookUserRaw);
const leftoverRaw = Object.hasOwn(req.body, 'leftover_from_meal_id') ? req.body.leftover_from_meal_id : req.body.leftoverFromMealId;
const vLeftoverFromMealId = validateLeftoverMealId(leftoverRaw);
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 (vLeftoverFromMealId.error) errors.push(vLeftoverFromMealId.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 });
const mealCategory = normalizeEnum(req.body.meal_category ?? req.body.mealCategory, VALID_MEAL_CATEGORIES, vLeftoverFromMealId.value ? 'leftovers' : 'other');
const protein = normalizeEnum(req.body.protein, VALID_PROTEINS, vLeftoverFromMealId.value ? 'none' : 'mixed');
const style = normalizeEnum(req.body.style, VALID_STYLES, vLeftoverFromMealId.value ? 'leftovers' : 'family');
if (vRecipeId.value !== null) {
const recipeExists = db.get().prepare('SELECT id FROM recipes WHERE id = ?').get(vRecipeId.value);
if (!recipeExists) return res.status(400).json({ error: 'Rezept nicht gefunden.', code: 400 });
@@ -251,9 +276,9 @@ router.post('/', (req, res) => {
const meal = db.transaction(() => {
const result = db.get().prepare(`
INSERT INTO meals (date, meal_type, title, notes, recipe_url, recipe_id, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(vDate.value, vType.value, vTitle.value, vNotes.value, vRecipeUrl.value, vRecipeId.value, currentUserId(req));
INSERT INTO meals (date, meal_type, title, notes, recipe_url, recipe_id, meal_category, protein, style, leftover_from_meal_id, source_plan_id, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(vDate.value, vType.value, vTitle.value, vNotes.value, vRecipeUrl.value, vRecipeId.value, mealCategory, protein, style, vLeftoverFromMealId.value, vSourcePlanId.value, currentUserId(req));
const mealId = result.lastInsertRowid;
@@ -316,9 +341,12 @@ router.put('/:id', (req, res) => {
if (req.body.recipe_id !== undefined) checks.push(num(req.body.recipe_id, 'Rezept-ID', { required: false }));
const cookUserRaw = Object.hasOwn(req.body, 'cook_user_id') ? req.body.cook_user_id : req.body.cookUserId;
const vCookUserId = validateCookUserId(cookUserRaw);
const leftoverRaw = Object.hasOwn(req.body, 'leftover_from_meal_id') ? req.body.leftover_from_meal_id : req.body.leftoverFromMealId;
const vLeftoverFromMealId = validateLeftoverMealId(leftoverRaw);
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 (vLeftoverFromMealId.error) errors.push(vLeftoverFromMealId.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 !== '') {
@@ -326,6 +354,12 @@ router.put('/:id', (req, res) => {
if (!recipeExists) return res.status(400).json({ error: 'Rezept nicht gefunden.', code: 400 });
}
const mealCategory = req.body.meal_category !== undefined || req.body.mealCategory !== undefined
? normalizeEnum(req.body.meal_category ?? req.body.mealCategory, VALID_MEAL_CATEGORIES, meal.meal_category || 'other')
: meal.meal_category;
const protein = req.body.protein !== undefined ? normalizeEnum(req.body.protein, VALID_PROTEINS, meal.protein || 'mixed') : meal.protein;
const style = req.body.style !== undefined ? normalizeEnum(req.body.style, VALID_STYLES, meal.style || 'family') : meal.style;
db.get().prepare(`
UPDATE meals
SET date = COALESCE(?, date),
@@ -333,7 +367,12 @@ router.put('/:id', (req, res) => {
title = COALESCE(?, title),
notes = ?,
recipe_url = ?,
recipe_id = ?
recipe_id = ?,
meal_category = ?,
protein = ?,
style = ?,
leftover_from_meal_id = ?,
source_plan_id = ?
WHERE id = ?
`).run(
req.body.date ?? null,
@@ -342,6 +381,11 @@ router.put('/:id', (req, res) => {
req.body.notes !== undefined ? (req.body.notes || null) : meal.notes,
req.body.recipe_url !== undefined ? (req.body.recipe_url || null) : meal.recipe_url,
req.body.recipe_id !== undefined ? (req.body.recipe_id || null) : meal.recipe_id,
mealCategory,
protein,
style,
vLeftoverFromMealId.present ? vLeftoverFromMealId.value : meal.leftover_from_meal_id,
req.body.source_plan_id !== undefined || req.body.sourcePlanId !== undefined ? vSourcePlanId.value : meal.source_plan_id,
id
);