From 1a73ed91dda0b04c91d861f243520cb6d53dd50d Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Mon, 27 Apr 2026 12:04:48 +0000 Subject: [PATCH] fix: compound shopping names + auto-Bring on depletion + panna da cucina MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. shopping_name compound-phrase map (computeShoppingName) Add phraseMap checked against the full product name BEFORE the single-token keyword loop. Prevents 'pane grattugiato' → 'Pane', 'panna da cucina' → 'Panna', etc. Key new phrases: - pane/pan grattugiato → Pangrattato - panna da cucina / panna cucina / panna chef → Panna da cucina - fette biscottate → Fette biscottate - aceto balsamico / glassa balsamico → Aceto balsamico - latte condensato/evaporato/vegetale/di soia/mandorla/avena/riso/cocco → specific - prosciutto cotto → Prosciutto cotto - farina di riso/mais/integrale → specific - pasta fresca, zucchero di canna, acqua minerale/frizzante/gassata, brodo, … Also added single-token safety-net entries: 'grattugiato'/'grattato'/'pangrattato' → 'Pangrattato', 'biscottate' → 'Fette biscottate'. 2. DB migration (sqlite3 UPDATE) Re-classified 10 products that had wrong shopping_name: Pane grattugiato → Pangrattato Panna da cucina (×4) → Panna da cucina Fette biscottate (×2) → Fette biscottate Aceto balsamico (×3) → Aceto balsamico Cleared 2 stale Gemini cache entries. 3. showLowStockBringPrompt (app.js) When totalRemaining <= 0 (product fully depleted), skip the modal entirely. The backend already auto-adds to Bring! on depletion; the JS only asks as a fallback if that failed (fire-and-forget async, never blocks the UI). The afterCallback (e.g. move-remainder modal, navigate to dashboard) is called immediately without user interaction. --- api/index.php | 69 +++++++++++++++++++++++++++++++++++ assets/js/app.js | 23 ++++++++++++ data/shopping_name_cache.json | 2 - 3 files changed, 92 insertions(+), 2 deletions(-) diff --git a/api/index.php b/api/index.php index fb81141..72c183f 100644 --- a/api/index.php +++ b/api/index.php @@ -3665,6 +3665,70 @@ function computeShoppingName(string $name, string $category = '', string $brand fn($w) => mb_strlen($w) > 2 && !in_array($w, $stop) )); + // 0. Compound-phrase map — checked against the FULL lowercase name (stop words included) + // so multi-word product types are classified BEFORE single-token lookup. + // This prevents "Pane grattugiato" → "Pane", "Panna da cucina" → "Panna", etc. + $phraseMap = [ + // Breadcrumbs (MUST come before generic "pane") + 'pangrattato' => 'Pangrattato', + 'pan grattato' => 'Pangrattato', + 'pane grattato' => 'Pangrattato', + 'pane grattugiato' => 'Pangrattato', + 'pan grattugiato' => 'Pangrattato', + // Cooking cream (MUST come before generic "panna") + 'panna da cucina' => 'Panna da cucina', + 'panna cucina' => 'Panna da cucina', + 'panna chef' => 'Panna da cucina', + 'panna acida' => 'Panna acida', + // Plant-based milks (MUST come before generic "latte") + 'latte condensato' => 'Latte condensato', + 'latte evaporato' => 'Latte condensato', + 'latte di soia' => 'Latte di soia', + 'latte soia' => 'Latte di soia', + 'latte vegetale' => 'Latte vegetale', + 'latte di mandorla' => 'Latte di mandorla', + 'latte mandorla' => 'Latte di mandorla', + 'latte di avena' => 'Latte di avena', + 'latte avena' => 'Latte di avena', + 'latte di riso' => 'Latte di riso', + 'latte riso' => 'Latte di riso', + 'latte di cocco' => 'Latte di cocco', + 'latte cocco' => 'Latte di cocco', + // Baked bakery — different from bread + 'fette biscottate' => 'Fette biscottate', + 'pan di spagna' => 'Pan di Spagna', + // Specific vinegars + 'aceto balsamico' => 'Aceto balsamico', + 'glassa balsamico' => 'Aceto balsamico', + 'glassa balsamic' => 'Aceto balsamico', + // Cold cuts — specific cuts + 'prosciutto cotto' => 'Prosciutto cotto', + // Flour subtypes + 'farina di riso' => 'Farina di riso', + 'farina riso' => 'Farina di riso', + 'farina di mais' => 'Farina di mais', + 'farina mais' => 'Farina di mais', + 'farina integrale' => 'Farina integrale', + // Fresh pasta + 'pasta fresca' => 'Pasta fresca', + // Broth / stock + 'brodo vegetale' => 'Brodo', + 'brodo pollo' => 'Brodo', + 'brodo manzo' => 'Brodo', + // Sugar subtypes + 'zucchero di canna' => 'Zucchero di canna', + 'zucchero canna' => 'Zucchero di canna', + // Water + 'acqua frizzante' => 'Acqua', + 'acqua gassata' => 'Acqua', + 'acqua minerale' => 'Acqua', + ]; + foreach ($phraseMap as $phrase => $canonical) { + if (mb_strpos($lower, $phrase) !== false) { + return $canonical; + } + } + // 1. Curated keyword → canonical group name. // Extended list covers the most common Italian pantry items and avoids Gemini calls. $keywordMap = [ @@ -3702,6 +3766,11 @@ function computeShoppingName(string $name, string $category = '', string $brand 'piadelle' => 'Piadina', 'biscotto' => 'Biscotti', 'biscotti' => 'Biscotti', + // Breadcrumbs single-token safety net (phrase map has priority, but just in case) + 'grattugiato' => 'Pangrattato', + 'grattato' => 'Pangrattato', + 'pangrattato' => 'Pangrattato', + 'biscottate' => 'Fette biscottate', // Dairy 'latte' => 'Latte', 'yogurt' => 'Yogurt', diff --git a/assets/js/app.js b/assets/js/app.js index 0097657..4cd5cac 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -5922,6 +5922,29 @@ function showLowStockBringPrompt(result, afterCallback) { const defaultQty = result.product_default_qty || parseFloat(currentProduct?.default_quantity) || 0; const totalRemaining = result.total_remaining; + // ── Fully depleted: no need to ask — backend already added to Bring! ── + // Skip the modal entirely and proceed to the next step (e.g. move modal). + if (totalRemaining <= 0) { + // Backend auto-adds to Bring! when fully depleted. If it failed (Bring not + // configured, or product already on list), silently attempt it from JS. + if (!result.added_to_bring && name) { + // Fire-and-forget — don't block the callback + (async () => { + try { + const spec = name; + const payload = { items: [{ name, specification: spec }] }; + if (shoppingListUUID) payload.listUUID = shoppingListUUID; + const data = await api('bring_add', {}, 'POST', payload); + if (data.success && data.added > 0) { + showToast('🛒 Prodotto finito → aggiunto a Bring!', 'info'); + } + } catch(_e) { /* silent */ } + })(); + } + if (afterCallback) afterCallback(); + return; + } + if (!isLowStock(totalRemaining, unit, defaultQty)) { if (afterCallback) afterCallback(); return; diff --git a/data/shopping_name_cache.json b/data/shopping_name_cache.json index b66064b..8a14bbc 100644 --- a/data/shopping_name_cache.json +++ b/data/shopping_name_cache.json @@ -9,8 +9,6 @@ "405ea6ec33d54042d046599650f422ea": "Succo", "f624c420f14d8eff122c0bb395eb63da": "Snack Dolci", "92751fbb97923590c402bc7810778b36": "Biscotti", - "0e342f4f977e814b2108e8e0475a57d5": "Aceto", - "edd038513b2641005bd36884f90789c1": "Pane", "8727f7abcb66764b5eb3d1f036bc18b8": "Tè", "0eb53fe1a5d4d106eac47c8a81d1afe7": "Farina", "0ebada5597d1d166d0ed8f49500bfeba": "Verdure",