feat: structure meal planning taxonomy and favorites
This commit is contained in:
+49
-5
@@ -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
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user