Skip to content

Commit 77f8db5

Browse files
JiroMusikclaude
andcommitted
Split 'Gewürze & Saucen' into separate categories, skip expiry for Gewürze
- New categories: 'Gewürze' (spices, don't expire) and 'Saucen' (sauces, do expire) - Migration splits existing items based on name pattern matching - mapCategory: cornichon/olive/kapern/essig/öl moved to Gewürze (not Saucen) - Dashboard: exclude 'Gewürze' from expiring-soon list - Frontend: getExpiryStatus skips warning for Gewürze category - Updated translations (de/en/es), Scanner dropdown, Inventory emoji map Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cb72e18 commit 77f8db5

8 files changed

Lines changed: 36 additions & 15 deletions

File tree

server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ app.get("/api/dashboard", (req, res) => {
299299
threeDaysLater.setDate(threeDaysLater.getDate() + 3);
300300
const threeDaysLaterStr = threeDaysLater.toISOString().split("T")[0];
301301

302-
const expiringSoon = db.prepare("SELECT * FROM items WHERE expiry_date <= ? AND quantity > 0 ORDER BY expiry_date ASC").all(threeDaysLaterStr);
302+
const expiringSoon = db.prepare("SELECT * FROM items WHERE expiry_date <= ? AND quantity > 0 AND category != 'Gewürze' ORDER BY expiry_date ASC").all(threeDaysLaterStr);
303303
const openedItems = db.prepare("SELECT * FROM items WHERE is_open = 1 AND quantity > 0 ORDER BY opened_at DESC").all();
304304
const todaysRecipes = db.prepare("SELECT * FROM planned_recipes WHERE date = ?").all(today).map((r: any) => ({
305305
...r,

server/db/migrations.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,18 @@ export const runMigrations = (db: Database) => {
129129
stmtLookup.run(u.new, u.old);
130130
}
131131

132+
// Split "Gewürze & Saucen" into separate categories
133+
const saucePattern = /sauce|ketchup|mayo|remoulade|senf|dressing|marinade|brühe|bouillon|fond|soja|worcester|tabasco|sriracha|pesto|saucenbinder|crema|aceto|balsamico/i;
134+
const oldCatItems = db.prepare("SELECT id, name FROM items WHERE category = 'Gewürze & Saucen'").all() as any[];
135+
const updateCat = db.prepare('UPDATE items SET category = ? WHERE id = ?');
136+
for (const item of oldCatItems) {
137+
if (saucePattern.test(item.name)) {
138+
updateCat.run('Saucen', item.id);
139+
} else {
140+
updateCat.run('Gewürze', item.id);
141+
}
142+
}
143+
132144
// Re-categorize all items for better consistency
133145
const items = db.prepare('SELECT id, name, category FROM items').all() as any[];
134146
const updateItemCat = db.prepare('UPDATE items SET category = ? WHERE id = ?');

server/utils/categories.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
export const mapCategory = (rawCategory: string, productName: string): string => {
22
const text = `${rawCategory} ${productName}`.toLowerCase();
33

4-
// Gewürze & Saucen ZUERST — viele Gewürze werden sonst als Gemüse/Fleisch/Getränke gematcht
5-
if (text.match(/gewürz|spice|sauce|condiment|salz\b|pfeffer|ketchup|mayo|remoulade|senf|essig|öl\b|olivenöl|dressing|marinade|brühe|bouillon|fond|soja|worcester|tabasco|sriracha|pesto|curry|kurkuma|kümmel|basilikum|rosmarin|oregano|thymian|petersilie|schnittlauch|dill|muskatnuss|paprika.*scharf|chili|peperoncin|ras el hanout|garam masala|zimt|nelke|anis|koriander|knoblauch.*granul|zwiebel.*pulver|sesam.*paste|tahina|saucenbinder|röstzwiebel|hackfleisch.*würz|steak.*pfeffer|pizza.*gewürz|pasta.*würz|bolognese.*gewürz|ankerkraut|fuchs|ostmann|ubena|cornichon|olive|kapern|gewürzzubereitung/)) return 'Gewürze & Saucen';
4+
// Saucen (werden schlecht — eigene Kategorie)
5+
if (text.match(/sauce|ketchup|mayo|remoulade|senf|dressing|marinade|brühe|bouillon|fond|soja.*sauce|worcester|tabasco|sriracha|pesto|saucenbinder|crema.*aceto|balsamico/)) return 'Saucen';
6+
// Gewürze (werden praktisch nicht schlecht)
7+
if (text.match(/gewürz|spice|condiment|salz\b|pfeffer|curry|kurkuma|kümmel|basilikum|rosmarin|oregano|thymian|petersilie|schnittlauch|dill|muskatnuss|paprika.*scharf|chili|peperoncin|ras el hanout|garam masala|zimt|nelke|anis|koriander|knoblauch.*granul|zwiebel.*pulver|sesam.*paste|tahina|röstzwiebel|hackfleisch.*würz|steak.*pfeffer|pizza.*gewürz|pasta.*würz|bolognese.*gewürz|ankerkraut|fuchs|ostmann|ubena|gewürzzubereitung|cornichon|olive|kapern|essig|öl\b|olivenöl/)) return 'Gewürze';
68
// Tiefkühl (hat Vorrang vor Fleisch/Fisch)
79
if (text.match(/frozen|tiefkühl|tiefgefroren|tk[ -]|ice cream|eis am stiel|pizza.*frozen|iglo|frosta|bofrost|gefrier|golden longs|rösti.*stäbchen/)) return 'Tiefkühl';
810
// Kühlregal

src/i18n/locales/de.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
"bakery": "Backwaren",
4545
"meatFish": "Fleisch & Fisch",
4646
"snacksSweets": "Snacks & Süßigkeiten",
47-
"spicesSauces": "Gewürze & Saucen",
47+
"spices": "Gewürze",
48+
"sauces": "Saucen",
4849
"householdDrugstore": "Haushalt & Drogerie",
4950
"other": "Sonstiges"
5051
},

src/i18n/locales/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
"bakery": "Bakery",
4545
"meatFish": "Meat & Fish",
4646
"snacksSweets": "Snacks & Sweets",
47-
"spicesSauces": "Spices & Sauces",
47+
"spices": "Spices",
48+
"sauces": "Sauces",
4849
"householdDrugstore": "Household & Drugstore",
4950
"other": "Other"
5051
},

src/i18n/locales/es.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
"bakery": "Panaderia",
4545
"meatFish": "Carne y pescado",
4646
"snacksSweets": "Snacks y dulces",
47-
"spicesSauces": "Especias y salsas",
47+
"spices": "Especias",
48+
"sauces": "Salsas",
4849
"householdDrugstore": "Hogar y drogueria",
4950
"other": "Otros"
5051
},

src/pages/Inventory.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import { useTranslation } from 'react-i18next';
88
export default function Inventory() {
99
const { t, i18n } = useTranslation();
1010

11-
const getExpiryStatus = (date: string | null) => {
11+
const getExpiryStatus = (date: string | null, category?: string) => {
1212
if (!date) return 'ok';
13+
// Gewürze werden praktisch nicht schlecht
14+
if (category === 'Gewürze') return 'ok';
1315
const expiry = new Date(date);
1416
const today = new Date();
1517
today.setHours(0, 0, 0, 0);
@@ -37,7 +39,8 @@ export default function Inventory() {
3739
'bakery': t('categories.bakery'),
3840
'meatFish': t('categories.meatFish'),
3941
'snacksSweets': t('categories.snacksSweets'),
40-
'spicesSauces': t('categories.spicesSauces'),
42+
'spices': t('categories.spices'),
43+
'sauces': t('categories.sauces'),
4144
'householdDrugstore': t('categories.householdDrugstore'),
4245
'other': t('categories.other'),
4346
};
@@ -233,8 +236,8 @@ export default function Inventory() {
233236
const sortedCategories = Object.keys(groupedByCategory).sort((a, b) => {
234237
const aItems = Object.values(groupedByCategory[a]).flat();
235238
const bItems = Object.values(groupedByCategory[b]).flat();
236-
const aHasWarning = aItems.some(i => getExpiryStatus(i.expiry_date) !== 'ok');
237-
const bHasWarning = bItems.some(i => getExpiryStatus(i.expiry_date) !== 'ok');
239+
const aHasWarning = aItems.some(i => getExpiryStatus(i.expiry_date, i.category) !== 'ok');
240+
const bHasWarning = bItems.some(i => getExpiryStatus(i.expiry_date, i.category) !== 'ok');
238241
if (aHasWarning && !bHasWarning) return -1;
239242
if (!aHasWarning && bHasWarning) return 1;
240243
return a.localeCompare(b);
@@ -294,7 +297,7 @@ export default function Inventory() {
294297
'Obst & Gemüse': '🥬', 'Kühlregal': '🧊', 'Tiefkühl': '❄️',
295298
'Vorratsschrank': '🏪', 'Getränke': '🥤', 'Backwaren': '🍞',
296299
'Fleisch & Fisch': '🥩', 'Snacks & Süßigkeiten': '🍫',
297-
'Gewürze & Saucen': '🧂', 'Haushalt & Drogerie': '🧹', 'Sonstiges': '📦'
300+
'Gewürze': '🧂', 'Saucen': '🫙', 'Haushalt & Drogerie': '🧹', 'Sonstiges': '📦'
298301
};
299302
const icon = categoryIcons[category] || '📦';
300303

@@ -515,10 +518,10 @@ export default function Inventory() {
515518
) : (
516519
<span className="bg-gray-100 text-gray-500 text-[10px] px-2 py-0.5 rounded-md font-bold shrink-0">{t('common.closed')}</span>
517520
)}
518-
{getExpiryStatus(item.expiry_date) === 'expired' && (
521+
{getExpiryStatus(item.expiry_date, item.category) === 'expired' && (
519522
<span className="bg-red-50 text-red-600 text-[10px] px-2 py-0.5 rounded-md font-black shrink-0">{t('common.expired')}</span>
520523
)}
521-
{getExpiryStatus(item.expiry_date) === 'warning' && (
524+
{getExpiryStatus(item.expiry_date, item.category) === 'warning' && (
522525
<span className="bg-orange-50 text-orange-600 text-[10px] px-2 py-0.5 rounded-md font-black shrink-0">{t('common.expiringSoon')}</span>
523526
)}
524527
</div>
@@ -530,7 +533,7 @@ export default function Inventory() {
530533
)}
531534
</span>
532535
{item.expiry_date && (
533-
<span className={`flex items-center gap-1 text-xs ${getExpiryStatus(item.expiry_date) !== 'ok' ? 'text-red-500 font-bold' : ''}`}>
536+
<span className={`flex items-center gap-1 text-xs ${getExpiryStatus(item.expiry_date, item.category) !== 'ok' ? 'text-red-500 font-bold' : ''}`}>
534537
<Calendar size={12} /> {formatDate(item.expiry_date)}
535538
</span>
536539
)}

src/pages/Scanner.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,8 @@ export default function Scanner() {
505505
<option value={t('categories.bakery')}>{t('categories.bakery')}</option>
506506
<option value={t('categories.meatFish')}>{t('categories.meatFish')}</option>
507507
<option value={t('categories.snacksSweets')}>{t('categories.snacksSweets')}</option>
508-
<option value={t('categories.spicesSauces')}>{t('categories.spicesSauces')}</option>
508+
<option value={t('categories.spices')}>{t('categories.spices')}</option>
509+
<option value={t('categories.sauces')}>{t('categories.sauces')}</option>
509510
<option value={t('categories.householdDrugstore')}>{t('categories.householdDrugstore')}</option>
510511
<option value={t('categories.other')}>{t('categories.other')}</option>
511512
</select>

0 commit comments

Comments
 (0)