feat: per-ingredient category selection for shopping list transfer (closes #33)

When adding ingredients in the meal editor, each ingredient now has a
category dropdown. Categories are stored on the ingredient and applied
automatically when transferring to the shopping list, so items appear
pre-grouped by category without manual re-sorting.
This commit is contained in:
Ulas
2026-04-15 07:11:49 +02:00
parent d6d2c41bfa
commit d16919ef7c
20 changed files with 97 additions and 32 deletions
+5
View File
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.19.6] - 2026-04-15
### Added
- Meals: ingredient category selection when adding ingredients to a meal - each ingredient can now be assigned a shopping category (e.g. Fruit & Vegetables, Dairy, Meat & Fish) directly in the meal editor. Categories are automatically applied when transferring ingredients to the shopping list, so items appear pre-sorted in their correct category groups (closes #33)
## [0.19.5] - 2026-04-14 ## [0.19.5] - 2026-04-14
### Fixed ### Fixed
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "oikos", "name": "oikos",
"version": "0.19.5", "version": "0.19.6",
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.", "description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
"main": "server/index.js", "main": "server/index.js",
"type": "module", "type": "module",
+3 -1
View File
@@ -226,6 +226,8 @@
"addIngredient": "إضافة مكون", "addIngredient": "إضافة مكون",
"ingredientNamePlaceholder": "المكون", "ingredientNamePlaceholder": "المكون",
"ingredientQtyPlaceholder": "الكمية", "ingredientQtyPlaceholder": "الكمية",
"ingredientCategoryLabel": "الفئة",
"ingredientCategoryDefault": "متنوعات",
"removeIngredient": "إزالة المكون", "removeIngredient": "إزالة المكون",
"transferLabel": "نقل المكونات إلى قائمة التسوق", "transferLabel": "نقل المكونات إلى قائمة التسوق",
"transferNow": "نقل الآن", "transferNow": "نقل الآن",
@@ -596,4 +598,4 @@
"unitMonth": "شهر", "unitMonth": "شهر",
"unitMonths": "أشهر" "unitMonths": "أشهر"
} }
} }
+2
View File
@@ -226,6 +226,8 @@
"addIngredient": "Zutat hinzufügen", "addIngredient": "Zutat hinzufügen",
"ingredientNamePlaceholder": "Zutat", "ingredientNamePlaceholder": "Zutat",
"ingredientQtyPlaceholder": "Menge", "ingredientQtyPlaceholder": "Menge",
"ingredientCategoryLabel": "Kategorie",
"ingredientCategoryDefault": "Sonstiges",
"removeIngredient": "Zutat entfernen", "removeIngredient": "Zutat entfernen",
"transferLabel": "Zutaten auf Einkaufsliste übertragen", "transferLabel": "Zutaten auf Einkaufsliste übertragen",
"transferNow": "Jetzt übertragen", "transferNow": "Jetzt übertragen",
+3 -1
View File
@@ -226,6 +226,8 @@
"addIngredient": "Προσθήκη υλικού", "addIngredient": "Προσθήκη υλικού",
"ingredientNamePlaceholder": "Υλικό", "ingredientNamePlaceholder": "Υλικό",
"ingredientQtyPlaceholder": "Ποσότητα", "ingredientQtyPlaceholder": "Ποσότητα",
"ingredientCategoryLabel": "Κατηγορία",
"ingredientCategoryDefault": "Διάφορα",
"removeIngredient": "Αφαίρεση υλικού", "removeIngredient": "Αφαίρεση υλικού",
"transferLabel": "Μεταφορά υλικών στη λίστα αγορών", "transferLabel": "Μεταφορά υλικών στη λίστα αγορών",
"transferNow": "Μεταφορά τώρα", "transferNow": "Μεταφορά τώρα",
@@ -596,4 +598,4 @@
"unitMonth": "μήνα", "unitMonth": "μήνα",
"unitMonths": "μήνες" "unitMonths": "μήνες"
} }
} }
+2
View File
@@ -226,6 +226,8 @@
"addIngredient": "Add ingredient", "addIngredient": "Add ingredient",
"ingredientNamePlaceholder": "Ingredient", "ingredientNamePlaceholder": "Ingredient",
"ingredientQtyPlaceholder": "Quantity", "ingredientQtyPlaceholder": "Quantity",
"ingredientCategoryLabel": "Category",
"ingredientCategoryDefault": "Miscellaneous",
"removeIngredient": "Remove ingredient", "removeIngredient": "Remove ingredient",
"transferLabel": "Transfer ingredients to shopping list", "transferLabel": "Transfer ingredients to shopping list",
"transferNow": "Transfer now", "transferNow": "Transfer now",
+3 -1
View File
@@ -226,6 +226,8 @@
"addIngredient": "Añadir ingrediente", "addIngredient": "Añadir ingrediente",
"ingredientNamePlaceholder": "Ingrediente", "ingredientNamePlaceholder": "Ingrediente",
"ingredientQtyPlaceholder": "Cantidad", "ingredientQtyPlaceholder": "Cantidad",
"ingredientCategoryLabel": "Categoría",
"ingredientCategoryDefault": "Varios",
"removeIngredient": "Eliminar ingrediente", "removeIngredient": "Eliminar ingrediente",
"transferLabel": "Transferir ingredientes a la lista de compras", "transferLabel": "Transferir ingredientes a la lista de compras",
"transferNow": "Transferir ahora", "transferNow": "Transferir ahora",
@@ -596,4 +598,4 @@
"unitMonth": "mes", "unitMonth": "mes",
"unitMonths": "meses" "unitMonths": "meses"
} }
} }
+3 -1
View File
@@ -226,6 +226,8 @@
"addIngredient": "Ajouter un ingrédient", "addIngredient": "Ajouter un ingrédient",
"ingredientNamePlaceholder": "Ingrédient", "ingredientNamePlaceholder": "Ingrédient",
"ingredientQtyPlaceholder": "Quantité", "ingredientQtyPlaceholder": "Quantité",
"ingredientCategoryLabel": "Catégorie",
"ingredientCategoryDefault": "Divers",
"removeIngredient": "Supprimer l'ingrédient", "removeIngredient": "Supprimer l'ingrédient",
"transferLabel": "Transférer les ingrédients vers la liste de courses", "transferLabel": "Transférer les ingrédients vers la liste de courses",
"transferNow": "Transférer maintenant", "transferNow": "Transférer maintenant",
@@ -596,4 +598,4 @@
"unitMonth": "mois", "unitMonth": "mois",
"unitMonths": "mois" "unitMonths": "mois"
} }
} }
+3 -1
View File
@@ -226,6 +226,8 @@
"addIngredient": "सामग्री जोड़ें", "addIngredient": "सामग्री जोड़ें",
"ingredientNamePlaceholder": "सामग्री", "ingredientNamePlaceholder": "सामग्री",
"ingredientQtyPlaceholder": "मात्रा", "ingredientQtyPlaceholder": "मात्रा",
"ingredientCategoryLabel": "श्रेणी",
"ingredientCategoryDefault": "विविध",
"removeIngredient": "सामग्री हटाएं", "removeIngredient": "सामग्री हटाएं",
"transferLabel": "सामग्री खरीदारी सूची में जोड़ें", "transferLabel": "सामग्री खरीदारी सूची में जोड़ें",
"transferNow": "अभी जोड़ें", "transferNow": "अभी जोड़ें",
@@ -596,4 +598,4 @@
"unitMonth": "माह", "unitMonth": "माह",
"unitMonths": "माह" "unitMonths": "माह"
} }
} }
+3 -1
View File
@@ -226,6 +226,8 @@
"addIngredient": "Aggiungi ingrediente", "addIngredient": "Aggiungi ingrediente",
"ingredientNamePlaceholder": "Ingrediente", "ingredientNamePlaceholder": "Ingrediente",
"ingredientQtyPlaceholder": "Quantità", "ingredientQtyPlaceholder": "Quantità",
"ingredientCategoryLabel": "Categoria",
"ingredientCategoryDefault": "Varie",
"removeIngredient": "Rimuovi ingrediente", "removeIngredient": "Rimuovi ingrediente",
"transferLabel": "Trasferisci ingredienti alla lista della spesa", "transferLabel": "Trasferisci ingredienti alla lista della spesa",
"transferNow": "Trasferisci ora", "transferNow": "Trasferisci ora",
@@ -596,4 +598,4 @@
"unitMonth": "mese", "unitMonth": "mese",
"unitMonths": "mesi" "unitMonths": "mesi"
} }
} }
+3 -1
View File
@@ -226,6 +226,8 @@
"addIngredient": "材料を追加", "addIngredient": "材料を追加",
"ingredientNamePlaceholder": "材料", "ingredientNamePlaceholder": "材料",
"ingredientQtyPlaceholder": "量", "ingredientQtyPlaceholder": "量",
"ingredientCategoryLabel": "カテゴリ",
"ingredientCategoryDefault": "その他",
"removeIngredient": "材料を削除", "removeIngredient": "材料を削除",
"transferLabel": "材料を買い物リストに追加", "transferLabel": "材料を買い物リストに追加",
"transferNow": "今すぐ追加", "transferNow": "今すぐ追加",
@@ -596,4 +598,4 @@
"unitMonth": "ヶ月", "unitMonth": "ヶ月",
"unitMonths": "ヶ月" "unitMonths": "ヶ月"
} }
} }
+3 -1
View File
@@ -226,6 +226,8 @@
"addIngredient": "Adicionar ingrediente", "addIngredient": "Adicionar ingrediente",
"ingredientNamePlaceholder": "Ingrediente", "ingredientNamePlaceholder": "Ingrediente",
"ingredientQtyPlaceholder": "Qtd", "ingredientQtyPlaceholder": "Qtd",
"ingredientCategoryLabel": "Categoria",
"ingredientCategoryDefault": "Outros",
"removeIngredient": "Remover ingrediente", "removeIngredient": "Remover ingrediente",
"transferLabel": "Transferir ingredientes para lista de compras", "transferLabel": "Transferir ingredientes para lista de compras",
"transferNow": "Transferir agora", "transferNow": "Transferir agora",
@@ -596,4 +598,4 @@
"unitMonth": "mês", "unitMonth": "mês",
"unitMonths": "meses" "unitMonths": "meses"
} }
} }
+3 -1
View File
@@ -226,6 +226,8 @@
"addIngredient": "Добавить ингредиент", "addIngredient": "Добавить ингредиент",
"ingredientNamePlaceholder": "Ингредиент", "ingredientNamePlaceholder": "Ингредиент",
"ingredientQtyPlaceholder": "Количество", "ingredientQtyPlaceholder": "Количество",
"ingredientCategoryLabel": "Категория",
"ingredientCategoryDefault": "Разное",
"removeIngredient": "Удалить ингредиент", "removeIngredient": "Удалить ингредиент",
"transferLabel": "Перенести ингредиенты в список покупок", "transferLabel": "Перенести ингредиенты в список покупок",
"transferNow": "Перенести сейчас", "transferNow": "Перенести сейчас",
@@ -596,4 +598,4 @@
"unitMonth": "месяц", "unitMonth": "месяц",
"unitMonths": "месяцев" "unitMonths": "месяцев"
} }
} }
+3 -1
View File
@@ -226,6 +226,8 @@
"addIngredient": "Tillsätt ingrediens", "addIngredient": "Tillsätt ingrediens",
"ingredientNamePlaceholder": "Ingrediens", "ingredientNamePlaceholder": "Ingrediens",
"ingredientQtyPlaceholder": "Kvantitet", "ingredientQtyPlaceholder": "Kvantitet",
"ingredientCategoryLabel": "Kategori",
"ingredientCategoryDefault": "Övrigt",
"removeIngredient": "Ta bort ingrediensen", "removeIngredient": "Ta bort ingrediensen",
"transferLabel": "Överför ingredienserna till inköpslistan", "transferLabel": "Överför ingredienserna till inköpslistan",
"transferNow": "Överför nu", "transferNow": "Överför nu",
@@ -596,4 +598,4 @@
"unitMonth": "månad", "unitMonth": "månad",
"unitMonths": "månader" "unitMonths": "månader"
} }
} }
+3 -1
View File
@@ -226,6 +226,8 @@
"addIngredient": "Malzeme ekle", "addIngredient": "Malzeme ekle",
"ingredientNamePlaceholder": "Malzeme", "ingredientNamePlaceholder": "Malzeme",
"ingredientQtyPlaceholder": "Miktar", "ingredientQtyPlaceholder": "Miktar",
"ingredientCategoryLabel": "Kategori",
"ingredientCategoryDefault": "Çeşitli",
"removeIngredient": "Malzemeyi kaldır", "removeIngredient": "Malzemeyi kaldır",
"transferLabel": "Malzemeleri alışveriş listesine aktar", "transferLabel": "Malzemeleri alışveriş listesine aktar",
"transferNow": "Şimdi aktar", "transferNow": "Şimdi aktar",
@@ -596,4 +598,4 @@
"unitMonth": "ay", "unitMonth": "ay",
"unitMonths": "ay" "unitMonths": "ay"
} }
} }
+3 -1
View File
@@ -226,6 +226,8 @@
"addIngredient": "添加食材", "addIngredient": "添加食材",
"ingredientNamePlaceholder": "食材", "ingredientNamePlaceholder": "食材",
"ingredientQtyPlaceholder": "数量", "ingredientQtyPlaceholder": "数量",
"ingredientCategoryLabel": "分类",
"ingredientCategoryDefault": "其他",
"removeIngredient": "移除食材", "removeIngredient": "移除食材",
"transferLabel": "将食材添加到购物清单", "transferLabel": "将食材添加到购物清单",
"transferNow": "立即添加", "transferNow": "立即添加",
@@ -596,4 +598,4 @@
"unitMonth": "个月", "unitMonth": "个月",
"unitMonths": "个月" "unitMonths": "个月"
} }
} }
+23 -7
View File
@@ -34,6 +34,7 @@ let state = {
currentWeek: null, // YYYY-MM-DD (Montag) currentWeek: null, // YYYY-MM-DD (Montag)
meals: [], meals: [],
lists: [], // Einkaufslisten für Transfer-Dropdown lists: [], // Einkaufslisten für Transfer-Dropdown
categories: [], // Einkaufskategorien für Zutaten
modal: null, modal: null,
visibleMealTypes: ['breakfast', 'lunch', 'dinner', 'snack'], visibleMealTypes: ['breakfast', 'lunch', 'dinner', 'snack'],
}; };
@@ -98,6 +99,15 @@ async function loadLists() {
} }
} }
async function loadCategories() {
try {
const res = await api.get('/shopping/categories');
state.categories = res.data;
} catch {
state.categories = [];
}
}
async function loadPreferences() { async function loadPreferences() {
try { try {
const res = await api.get('/preferences'); const res = await api.get('/preferences');
@@ -137,7 +147,7 @@ export async function render(container, { user }) {
const today = new Date().toISOString().slice(0, 10); const today = new Date().toISOString().slice(0, 10);
const monday = getMondayOf(today); const monday = getMondayOf(today);
await Promise.all([loadWeek(monday), loadLists(), loadPreferences()]); await Promise.all([loadWeek(monday), loadLists(), loadPreferences(), loadCategories()]);
renderWeekGrid(); renderWeekGrid();
wireNav(); wireNav();
} }
@@ -554,7 +564,7 @@ function buildModalContent({ mode, date, mealType, meal }) {
: `<option value="" disabled>${t('meals.noShoppingLists')}</option>`; : `<option value="" disabled>${t('meals.noShoppingLists')}</option>`;
const ingRows = isEdit && meal.ingredients?.length const ingRows = isEdit && meal.ingredients?.length
? meal.ingredients.map((ing) => ingredientRowHTML(ing.name, ing.quantity ?? '', ing.id)).join('') ? meal.ingredients.map((ing) => ingredientRowHTML(ing.name, ing.quantity ?? '', ing.id, ing.category ?? 'Sonstiges')).join('')
: ''; : '';
const hasIngOpen = isEdit && meal.ingredients?.some((i) => !i.on_shopping_list); const hasIngOpen = isEdit && meal.ingredients?.some((i) => !i.on_shopping_list);
@@ -620,11 +630,16 @@ function buildModalContent({ mode, date, mealType, meal }) {
</div>`; </div>`;
} }
function ingredientRowHTML(name, qty, id) { function ingredientRowHTML(name, qty, id, category = 'Sonstiges') {
const catOptions = state.categories.length
? state.categories.map((c) => `<option value="${esc(c.name)}" ${c.name === category ? 'selected' : ''}>${esc(c.name)}</option>`).join('')
: `<option value="Sonstiges" selected>${t('meals.ingredientCategoryDefault')}</option>`;
return ` return `
<div class="ingredient-row" data-ing-id="${id ?? ''}"> <div class="ingredient-row" data-ing-id="${id ?? ''}">
<input type="text" class="form-input ingredient-row__name" placeholder="${t('meals.ingredientNamePlaceholder')}" value="${esc(name)}"> <input type="text" class="form-input ingredient-row__name" placeholder="${t('meals.ingredientNamePlaceholder')}" value="${esc(name)}">
<input type="text" class="form-input ingredient-row__qty" placeholder="${t('meals.ingredientQtyPlaceholder')}" value="${esc(qty)}"> <input type="text" class="form-input ingredient-row__qty" placeholder="${t('meals.ingredientQtyPlaceholder')}" value="${esc(qty)}">
<select class="form-input ingredient-row__cat" aria-label="${t('meals.ingredientCategoryLabel')}">${catOptions}</select>
<button class="ingredient-row__remove" data-action="remove-ingredient" type="button" aria-label="${t('meals.removeIngredient')}"> <button class="ingredient-row__remove" data-action="remove-ingredient" type="button" aria-label="${t('meals.removeIngredient')}">
<i data-lucide="x" style="width:14px;height:14px;" aria-hidden="true"></i> <i data-lucide="x" style="width:14px;height:14px;" aria-hidden="true"></i>
</button> </button>
@@ -652,9 +667,10 @@ async function saveModal(overlay) {
const ingredients = []; const ingredients = [];
overlay.querySelectorAll('.ingredient-row').forEach((row) => { overlay.querySelectorAll('.ingredient-row').forEach((row) => {
const name = row.querySelector('.ingredient-row__name').value.trim(); const name = row.querySelector('.ingredient-row__name').value.trim();
const qty = row.querySelector('.ingredient-row__qty').value.trim() || null; const qty = row.querySelector('.ingredient-row__qty').value.trim() || null;
if (name) ingredients.push({ name, quantity: qty, id: row.dataset.ingId || null }); const category = row.querySelector('.ingredient-row__cat')?.value || 'Sonstiges';
if (name) ingredients.push({ name, quantity: qty, category, id: row.dataset.ingId || null });
}); });
saveBtn.disabled = true; saveBtn.disabled = true;
@@ -680,7 +696,7 @@ async function saveModal(overlay) {
if (!keptIds.has(id)) await api.delete(`/meals/ingredients/${id}`); if (!keptIds.has(id)) await api.delete(`/meals/ingredients/${id}`);
} }
for (const ing of ingredients) { for (const ing of ingredients) {
if (!ing.id) await api.post(`/meals/${meal.id}/ingredients`, { name: ing.name, quantity: ing.quantity }); if (!ing.id) await api.post(`/meals/${meal.id}/ingredients`, { name: ing.name, quantity: ing.quantity, category: ing.category });
} }
// Reload updated meal // Reload updated meal
+6
View File
@@ -329,6 +329,12 @@
min-width: 0; min-width: 0;
} }
.ingredient-row__cat {
flex: 1.5;
min-width: 0;
font-size: var(--text-sm);
}
.ingredient-row__remove { .ingredient-row__remove {
width: 32px; width: 32px;
height: 32px; height: 32px;
+7
View File
@@ -362,6 +362,13 @@ const MIGRATIONS = [
ALTER TABLE meals ADD COLUMN recipe_url TEXT; ALTER TABLE meals ADD COLUMN recipe_url TEXT;
`, `,
}, },
{
version: 7,
description: 'Kategorie pro Zutat für Einkaufslisten-Transfer',
up: `
ALTER TABLE meal_ingredients ADD COLUMN category TEXT NOT NULL DEFAULT 'Sonstiges';
`,
},
]; ];
/** /**
+15 -12
View File
@@ -169,13 +169,14 @@ router.post('/', (req, res) => {
const mealId = result.lastInsertRowid; const mealId = result.lastInsertRowid;
const insertIng = db.get().prepare(` const insertIng = db.get().prepare(`
INSERT INTO meal_ingredients (meal_id, name, quantity) VALUES (?, ?, ?) INSERT INTO meal_ingredients (meal_id, name, quantity, category) VALUES (?, ?, ?, ?)
`); `);
for (const ing of ingredients) { for (const ing of ingredients) {
const name = String(ing.name || '').trim().slice(0, MAX_TITLE); const name = String(ing.name || '').trim().slice(0, MAX_TITLE);
const qty = String(ing.quantity || '').trim().slice(0, MAX_SHORT) || null; const qty = String(ing.quantity || '').trim().slice(0, MAX_SHORT) || null;
if (name) insertIng.run(mealId, name, qty); const category = String(ing.category || '').trim().slice(0, MAX_SHORT) || 'Sonstiges';
if (name) insertIng.run(mealId, name, qty, category);
} }
return db.get().prepare(` return db.get().prepare(`
@@ -287,13 +288,13 @@ router.post('/:id/ingredients', (req, res) => {
const meal = db.get().prepare('SELECT id FROM meals WHERE id = ?').get(mealId); const meal = db.get().prepare('SELECT id FROM meals WHERE id = ?').get(mealId);
if (!meal) return res.status(404).json({ error: 'Mahlzeit nicht gefunden', code: 404 }); if (!meal) return res.status(404).json({ error: 'Mahlzeit nicht gefunden', code: 404 });
const { name, quantity = null } = req.body; const { name, quantity = null, category = 'Sonstiges' } = req.body;
if (!name || !name.trim()) if (!name || !name.trim())
return res.status(400).json({ error: 'Name ist erforderlich', code: 400 }); return res.status(400).json({ error: 'Name ist erforderlich', code: 400 });
const result = db.get().prepare(` const result = db.get().prepare(`
INSERT INTO meal_ingredients (meal_id, name, quantity) VALUES (?, ?, ?) INSERT INTO meal_ingredients (meal_id, name, quantity, category) VALUES (?, ?, ?, ?)
`).run(mealId, name.trim(), quantity?.trim() || null); `).run(mealId, name.trim(), quantity?.trim() || null, String(category || '').trim() || 'Sonstiges');
const ing = db.get().prepare( const ing = db.get().prepare(
'SELECT * FROM meal_ingredients WHERE id = ?' 'SELECT * FROM meal_ingredients WHERE id = ?'
@@ -318,17 +319,19 @@ router.patch('/ingredients/:ingId', (req, res) => {
const ing = db.get().prepare('SELECT * FROM meal_ingredients WHERE id = ?').get(ingId); const ing = db.get().prepare('SELECT * FROM meal_ingredients WHERE id = ?').get(ingId);
if (!ing) return res.status(404).json({ error: 'Zutat nicht gefunden', code: 404 }); if (!ing) return res.status(404).json({ error: 'Zutat nicht gefunden', code: 404 });
const { name, quantity, on_shopping_list } = req.body; const { name, quantity, on_shopping_list, category } = req.body;
db.get().prepare(` db.get().prepare(`
UPDATE meal_ingredients UPDATE meal_ingredients
SET name = COALESCE(?, name), SET name = COALESCE(?, name),
quantity = ?, quantity = ?,
category = COALESCE(?, category),
on_shopping_list = COALESCE(?, on_shopping_list) on_shopping_list = COALESCE(?, on_shopping_list)
WHERE id = ? WHERE id = ?
`).run( `).run(
name?.trim() ?? null, name?.trim() ?? null,
quantity !== undefined ? (quantity?.trim() || null) : ing.quantity, quantity !== undefined ? (quantity?.trim() || null) : ing.quantity,
category !== undefined ? (String(category || '').trim() || 'Sonstiges') : null,
on_shopping_list !== undefined ? (on_shopping_list ? 1 : 0) : null, on_shopping_list !== undefined ? (on_shopping_list ? 1 : 0) : null,
ingId ingId
); );
@@ -378,7 +381,7 @@ router.post('/:id/to-shopping-list', (req, res) => {
const meal = db.get().prepare('SELECT id FROM meals WHERE id = ?').get(mealId); const meal = db.get().prepare('SELECT id FROM meals WHERE id = ?').get(mealId);
if (!meal) return res.status(404).json({ error: 'Mahlzeit nicht gefunden', code: 404 }); if (!meal) return res.status(404).json({ error: 'Mahlzeit nicht gefunden', code: 404 });
const { listId, category = 'Sonstiges' } = req.body; const { listId } = req.body;
if (!listId) if (!listId)
return res.status(400).json({ error: 'listId ist erforderlich', code: 400 }); return res.status(400).json({ error: 'listId ist erforderlich', code: 400 });
@@ -404,7 +407,7 @@ router.post('/:id/to-shopping-list', (req, res) => {
let count = 0; let count = 0;
for (const ing of ingredients) { for (const ing of ingredients) {
insertItem.run(listId, ing.name, ing.quantity, category, mealId); insertItem.run(listId, ing.name, ing.quantity, ing.category || 'Sonstiges', mealId);
markDone.run(ing.id); markDone.run(ing.id);
count++; count++;
} }
@@ -426,7 +429,7 @@ router.post('/:id/to-shopping-list', (req, res) => {
*/ */
router.post('/week-to-shopping-list', (req, res) => { router.post('/week-to-shopping-list', (req, res) => {
try { try {
const { listId, week, category = 'Sonstiges' } = req.body; const { listId, week } = req.body;
if (!listId) if (!listId)
return res.status(400).json({ error: 'listId ist erforderlich', code: 400 }); return res.status(400).json({ error: 'listId ist erforderlich', code: 400 });
@@ -460,7 +463,7 @@ router.post('/week-to-shopping-list', (req, res) => {
let count = 0; let count = 0;
for (const ing of ingredients) { for (const ing of ingredients) {
insertItem.run(listId, ing.name, ing.quantity, category, ing.meal_id); insertItem.run(listId, ing.name, ing.quantity, ing.category || 'Sonstiges', ing.meal_id);
markDone.run(ing.id); markDone.run(ing.id);
count++; count++;
} }