diff --git a/CHANGELOG.md b/CHANGELOG.md index 9307fd5..d527ab2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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 ### Fixed diff --git a/package.json b/package.json index 7122007..ec531a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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.", "main": "server/index.js", "type": "module", diff --git a/public/locales/ar.json b/public/locales/ar.json index 854ffa2..1b00c2e 100644 --- a/public/locales/ar.json +++ b/public/locales/ar.json @@ -226,6 +226,8 @@ "addIngredient": "إضافة مكون", "ingredientNamePlaceholder": "المكون", "ingredientQtyPlaceholder": "الكمية", + "ingredientCategoryLabel": "الفئة", + "ingredientCategoryDefault": "متنوعات", "removeIngredient": "إزالة المكون", "transferLabel": "نقل المكونات إلى قائمة التسوق", "transferNow": "نقل الآن", @@ -596,4 +598,4 @@ "unitMonth": "شهر", "unitMonths": "أشهر" } -} +} \ No newline at end of file diff --git a/public/locales/de.json b/public/locales/de.json index 63a7d0f..94470bd 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -226,6 +226,8 @@ "addIngredient": "Zutat hinzufügen", "ingredientNamePlaceholder": "Zutat", "ingredientQtyPlaceholder": "Menge", + "ingredientCategoryLabel": "Kategorie", + "ingredientCategoryDefault": "Sonstiges", "removeIngredient": "Zutat entfernen", "transferLabel": "Zutaten auf Einkaufsliste übertragen", "transferNow": "Jetzt übertragen", diff --git a/public/locales/el.json b/public/locales/el.json index 87b0eda..bd22908 100644 --- a/public/locales/el.json +++ b/public/locales/el.json @@ -226,6 +226,8 @@ "addIngredient": "Προσθήκη υλικού", "ingredientNamePlaceholder": "Υλικό", "ingredientQtyPlaceholder": "Ποσότητα", + "ingredientCategoryLabel": "Κατηγορία", + "ingredientCategoryDefault": "Διάφορα", "removeIngredient": "Αφαίρεση υλικού", "transferLabel": "Μεταφορά υλικών στη λίστα αγορών", "transferNow": "Μεταφορά τώρα", @@ -596,4 +598,4 @@ "unitMonth": "μήνα", "unitMonths": "μήνες" } -} +} \ No newline at end of file diff --git a/public/locales/en.json b/public/locales/en.json index 3df0b55..21e257f 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -226,6 +226,8 @@ "addIngredient": "Add ingredient", "ingredientNamePlaceholder": "Ingredient", "ingredientQtyPlaceholder": "Quantity", + "ingredientCategoryLabel": "Category", + "ingredientCategoryDefault": "Miscellaneous", "removeIngredient": "Remove ingredient", "transferLabel": "Transfer ingredients to shopping list", "transferNow": "Transfer now", diff --git a/public/locales/es.json b/public/locales/es.json index 6790aa7..b4dbe21 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -226,6 +226,8 @@ "addIngredient": "Añadir ingrediente", "ingredientNamePlaceholder": "Ingrediente", "ingredientQtyPlaceholder": "Cantidad", + "ingredientCategoryLabel": "Categoría", + "ingredientCategoryDefault": "Varios", "removeIngredient": "Eliminar ingrediente", "transferLabel": "Transferir ingredientes a la lista de compras", "transferNow": "Transferir ahora", @@ -596,4 +598,4 @@ "unitMonth": "mes", "unitMonths": "meses" } -} +} \ No newline at end of file diff --git a/public/locales/fr.json b/public/locales/fr.json index 35daea5..efaa4c2 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -226,6 +226,8 @@ "addIngredient": "Ajouter un ingrédient", "ingredientNamePlaceholder": "Ingrédient", "ingredientQtyPlaceholder": "Quantité", + "ingredientCategoryLabel": "Catégorie", + "ingredientCategoryDefault": "Divers", "removeIngredient": "Supprimer l'ingrédient", "transferLabel": "Transférer les ingrédients vers la liste de courses", "transferNow": "Transférer maintenant", @@ -596,4 +598,4 @@ "unitMonth": "mois", "unitMonths": "mois" } -} +} \ No newline at end of file diff --git a/public/locales/hi.json b/public/locales/hi.json index 860e6d3..309e5c9 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -226,6 +226,8 @@ "addIngredient": "सामग्री जोड़ें", "ingredientNamePlaceholder": "सामग्री", "ingredientQtyPlaceholder": "मात्रा", + "ingredientCategoryLabel": "श्रेणी", + "ingredientCategoryDefault": "विविध", "removeIngredient": "सामग्री हटाएं", "transferLabel": "सामग्री खरीदारी सूची में जोड़ें", "transferNow": "अभी जोड़ें", @@ -596,4 +598,4 @@ "unitMonth": "माह", "unitMonths": "माह" } -} +} \ No newline at end of file diff --git a/public/locales/it.json b/public/locales/it.json index 5c9bcad..d9aab09 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -226,6 +226,8 @@ "addIngredient": "Aggiungi ingrediente", "ingredientNamePlaceholder": "Ingrediente", "ingredientQtyPlaceholder": "Quantità", + "ingredientCategoryLabel": "Categoria", + "ingredientCategoryDefault": "Varie", "removeIngredient": "Rimuovi ingrediente", "transferLabel": "Trasferisci ingredienti alla lista della spesa", "transferNow": "Trasferisci ora", @@ -596,4 +598,4 @@ "unitMonth": "mese", "unitMonths": "mesi" } -} +} \ No newline at end of file diff --git a/public/locales/ja.json b/public/locales/ja.json index f18defb..2a5c5c1 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -226,6 +226,8 @@ "addIngredient": "材料を追加", "ingredientNamePlaceholder": "材料", "ingredientQtyPlaceholder": "量", + "ingredientCategoryLabel": "カテゴリ", + "ingredientCategoryDefault": "その他", "removeIngredient": "材料を削除", "transferLabel": "材料を買い物リストに追加", "transferNow": "今すぐ追加", @@ -596,4 +598,4 @@ "unitMonth": "ヶ月", "unitMonths": "ヶ月" } -} +} \ No newline at end of file diff --git a/public/locales/pt.json b/public/locales/pt.json index 449c5df..a0c5960 100644 --- a/public/locales/pt.json +++ b/public/locales/pt.json @@ -226,6 +226,8 @@ "addIngredient": "Adicionar ingrediente", "ingredientNamePlaceholder": "Ingrediente", "ingredientQtyPlaceholder": "Qtd", + "ingredientCategoryLabel": "Categoria", + "ingredientCategoryDefault": "Outros", "removeIngredient": "Remover ingrediente", "transferLabel": "Transferir ingredientes para lista de compras", "transferNow": "Transferir agora", @@ -596,4 +598,4 @@ "unitMonth": "mês", "unitMonths": "meses" } -} +} \ No newline at end of file diff --git a/public/locales/ru.json b/public/locales/ru.json index 1b6af89..2b4405f 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -226,6 +226,8 @@ "addIngredient": "Добавить ингредиент", "ingredientNamePlaceholder": "Ингредиент", "ingredientQtyPlaceholder": "Количество", + "ingredientCategoryLabel": "Категория", + "ingredientCategoryDefault": "Разное", "removeIngredient": "Удалить ингредиент", "transferLabel": "Перенести ингредиенты в список покупок", "transferNow": "Перенести сейчас", @@ -596,4 +598,4 @@ "unitMonth": "месяц", "unitMonths": "месяцев" } -} +} \ No newline at end of file diff --git a/public/locales/sv.json b/public/locales/sv.json index bf05c1b..4693c2c 100644 --- a/public/locales/sv.json +++ b/public/locales/sv.json @@ -226,6 +226,8 @@ "addIngredient": "Tillsätt ingrediens", "ingredientNamePlaceholder": "Ingrediens", "ingredientQtyPlaceholder": "Kvantitet", + "ingredientCategoryLabel": "Kategori", + "ingredientCategoryDefault": "Övrigt", "removeIngredient": "Ta bort ingrediensen", "transferLabel": "Överför ingredienserna till inköpslistan", "transferNow": "Överför nu", @@ -596,4 +598,4 @@ "unitMonth": "månad", "unitMonths": "månader" } -} +} \ No newline at end of file diff --git a/public/locales/tr.json b/public/locales/tr.json index 0d7b267..c9ce1ac 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -226,6 +226,8 @@ "addIngredient": "Malzeme ekle", "ingredientNamePlaceholder": "Malzeme", "ingredientQtyPlaceholder": "Miktar", + "ingredientCategoryLabel": "Kategori", + "ingredientCategoryDefault": "Çeşitli", "removeIngredient": "Malzemeyi kaldır", "transferLabel": "Malzemeleri alışveriş listesine aktar", "transferNow": "Şimdi aktar", @@ -596,4 +598,4 @@ "unitMonth": "ay", "unitMonths": "ay" } -} +} \ No newline at end of file diff --git a/public/locales/zh.json b/public/locales/zh.json index 06a3c9d..9a7b717 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -226,6 +226,8 @@ "addIngredient": "添加食材", "ingredientNamePlaceholder": "食材", "ingredientQtyPlaceholder": "数量", + "ingredientCategoryLabel": "分类", + "ingredientCategoryDefault": "其他", "removeIngredient": "移除食材", "transferLabel": "将食材添加到购物清单", "transferNow": "立即添加", @@ -596,4 +598,4 @@ "unitMonth": "个月", "unitMonths": "个月" } -} +} \ No newline at end of file diff --git a/public/pages/meals.js b/public/pages/meals.js index 07dcfb9..1e5a0bf 100644 --- a/public/pages/meals.js +++ b/public/pages/meals.js @@ -34,6 +34,7 @@ let state = { currentWeek: null, // YYYY-MM-DD (Montag) meals: [], lists: [], // Einkaufslisten für Transfer-Dropdown + categories: [], // Einkaufskategorien für Zutaten modal: null, 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() { try { 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 monday = getMondayOf(today); - await Promise.all([loadWeek(monday), loadLists(), loadPreferences()]); + await Promise.all([loadWeek(monday), loadLists(), loadPreferences(), loadCategories()]); renderWeekGrid(); wireNav(); } @@ -554,7 +564,7 @@ function buildModalContent({ mode, date, mealType, meal }) { : ``; 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); @@ -620,11 +630,16 @@ function buildModalContent({ mode, date, mealType, meal }) { `; } -function ingredientRowHTML(name, qty, id) { +function ingredientRowHTML(name, qty, id, category = 'Sonstiges') { + const catOptions = state.categories.length + ? state.categories.map((c) => ``).join('') + : ``; + return `