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:
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -226,6 +226,8 @@
|
|||||||
"addIngredient": "إضافة مكون",
|
"addIngredient": "إضافة مكون",
|
||||||
"ingredientNamePlaceholder": "المكون",
|
"ingredientNamePlaceholder": "المكون",
|
||||||
"ingredientQtyPlaceholder": "الكمية",
|
"ingredientQtyPlaceholder": "الكمية",
|
||||||
|
"ingredientCategoryLabel": "الفئة",
|
||||||
|
"ingredientCategoryDefault": "متنوعات",
|
||||||
"removeIngredient": "إزالة المكون",
|
"removeIngredient": "إزالة المكون",
|
||||||
"transferLabel": "نقل المكونات إلى قائمة التسوق",
|
"transferLabel": "نقل المكونات إلى قائمة التسوق",
|
||||||
"transferNow": "نقل الآن",
|
"transferNow": "نقل الآن",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -226,6 +226,8 @@
|
|||||||
"addIngredient": "Προσθήκη υλικού",
|
"addIngredient": "Προσθήκη υλικού",
|
||||||
"ingredientNamePlaceholder": "Υλικό",
|
"ingredientNamePlaceholder": "Υλικό",
|
||||||
"ingredientQtyPlaceholder": "Ποσότητα",
|
"ingredientQtyPlaceholder": "Ποσότητα",
|
||||||
|
"ingredientCategoryLabel": "Κατηγορία",
|
||||||
|
"ingredientCategoryDefault": "Διάφορα",
|
||||||
"removeIngredient": "Αφαίρεση υλικού",
|
"removeIngredient": "Αφαίρεση υλικού",
|
||||||
"transferLabel": "Μεταφορά υλικών στη λίστα αγορών",
|
"transferLabel": "Μεταφορά υλικών στη λίστα αγορών",
|
||||||
"transferNow": "Μεταφορά τώρα",
|
"transferNow": "Μεταφορά τώρα",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -226,6 +226,8 @@
|
|||||||
"addIngredient": "सामग्री जोड़ें",
|
"addIngredient": "सामग्री जोड़ें",
|
||||||
"ingredientNamePlaceholder": "सामग्री",
|
"ingredientNamePlaceholder": "सामग्री",
|
||||||
"ingredientQtyPlaceholder": "मात्रा",
|
"ingredientQtyPlaceholder": "मात्रा",
|
||||||
|
"ingredientCategoryLabel": "श्रेणी",
|
||||||
|
"ingredientCategoryDefault": "विविध",
|
||||||
"removeIngredient": "सामग्री हटाएं",
|
"removeIngredient": "सामग्री हटाएं",
|
||||||
"transferLabel": "सामग्री खरीदारी सूची में जोड़ें",
|
"transferLabel": "सामग्री खरीदारी सूची में जोड़ें",
|
||||||
"transferNow": "अभी जोड़ें",
|
"transferNow": "अभी जोड़ें",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -226,6 +226,8 @@
|
|||||||
"addIngredient": "材料を追加",
|
"addIngredient": "材料を追加",
|
||||||
"ingredientNamePlaceholder": "材料",
|
"ingredientNamePlaceholder": "材料",
|
||||||
"ingredientQtyPlaceholder": "量",
|
"ingredientQtyPlaceholder": "量",
|
||||||
|
"ingredientCategoryLabel": "カテゴリ",
|
||||||
|
"ingredientCategoryDefault": "その他",
|
||||||
"removeIngredient": "材料を削除",
|
"removeIngredient": "材料を削除",
|
||||||
"transferLabel": "材料を買い物リストに追加",
|
"transferLabel": "材料を買い物リストに追加",
|
||||||
"transferNow": "今すぐ追加",
|
"transferNow": "今すぐ追加",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -226,6 +226,8 @@
|
|||||||
"addIngredient": "Добавить ингредиент",
|
"addIngredient": "Добавить ингредиент",
|
||||||
"ingredientNamePlaceholder": "Ингредиент",
|
"ingredientNamePlaceholder": "Ингредиент",
|
||||||
"ingredientQtyPlaceholder": "Количество",
|
"ingredientQtyPlaceholder": "Количество",
|
||||||
|
"ingredientCategoryLabel": "Категория",
|
||||||
|
"ingredientCategoryDefault": "Разное",
|
||||||
"removeIngredient": "Удалить ингредиент",
|
"removeIngredient": "Удалить ингредиент",
|
||||||
"transferLabel": "Перенести ингредиенты в список покупок",
|
"transferLabel": "Перенести ингредиенты в список покупок",
|
||||||
"transferNow": "Перенести сейчас",
|
"transferNow": "Перенести сейчас",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -226,6 +226,8 @@
|
|||||||
"addIngredient": "添加食材",
|
"addIngredient": "添加食材",
|
||||||
"ingredientNamePlaceholder": "食材",
|
"ingredientNamePlaceholder": "食材",
|
||||||
"ingredientQtyPlaceholder": "数量",
|
"ingredientQtyPlaceholder": "数量",
|
||||||
|
"ingredientCategoryLabel": "分类",
|
||||||
|
"ingredientCategoryDefault": "其他",
|
||||||
"removeIngredient": "移除食材",
|
"removeIngredient": "移除食材",
|
||||||
"transferLabel": "将食材添加到购物清单",
|
"transferLabel": "将食材添加到购物清单",
|
||||||
"transferNow": "立即添加",
|
"transferNow": "立即添加",
|
||||||
|
|||||||
+21
-5
@@ -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>
|
||||||
@@ -654,7 +669,8 @@ async function saveModal(overlay) {
|
|||||||
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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+13
-10
@@ -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++;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user