feat: generic shopping names — group products by shopping_name

- Add shopping_name column to products table
- Add computeShoppingName() PHP auto-assign function:
  * Curated keyword map: all salumi/cold cuts → 'Affettato'
  * Bring! catalog back-translation: 'Latte di Montagna' → 'Latte'
  * Fallback: first significant token capitalized
- Migrate all 210 existing products with auto-computed shopping_name
- saveProduct() auto-computes shopping_name on every create/update
- smartShopping() groups items by shopping_name: most urgent item is
  representative, others listed as variants (e.g. 'Affettato' shows
  Mortadella, Speck, Nduja, Salame, Prosciutto, Schinkenspeck as one row)
- _productOnBring() also checks shopping_name for Bring! match detection
- addToInventory auto-remove: uses shopping_name-based Bring! key
- useFromInventory auto-add: sends shopping_name to Bring! (not raw name),
  specific product name goes into specification field
- Frontend renderSmartItem: shows shopping_name as title, specific
  product name(s) in italic subtitle line below
- _syncOnBringFlags: matches on both name and shopping_name
This commit is contained in:
dadaloop82
2026-04-27 08:16:44 +00:00
parent 61e94db0d3
commit 430f9e7854
3 changed files with 209 additions and 39 deletions
+36 -7
View File
@@ -7167,12 +7167,13 @@ function _syncOnBringFlags() {
for (const si of smartShoppingItems) {
const siLower = si.name.toLowerCase();
const siFirst = _nameTokens(si.name)[0];
const siShoppingLower = (si.shopping_name || '').toLowerCase();
const siShoppingFirst = si.shopping_name ? _nameTokens(si.shopping_name)[0] : null;
si.on_bring = !!(
shoppingItems.find(bi => bi.name.toLowerCase() === siLower) ||
(siFirst && shoppingItems.find(bi => {
const biFirst = _nameTokens(bi.name)[0];
return biFirst === siFirst;
}))
(siShoppingLower && shoppingItems.find(bi => bi.name.toLowerCase() === siShoppingLower)) ||
(siFirst && shoppingItems.find(bi => _nameTokens(bi.name)[0] === siFirst)) ||
(siShoppingFirst && shoppingItems.find(bi => _nameTokens(bi.name)[0] === siShoppingFirst))
);
}
}
@@ -7292,6 +7293,27 @@ function renderSmartItem(item) {
const catIcon = CATEGORY_ICONS[mapToLocalCategory(item.category, item.name)] || '📦';
const globalIdx = smartShoppingItems.indexOf(item);
// Generic vs specific name logic
const shoppingName = item.shopping_name || item.name;
const isGeneric = shoppingName !== item.name;
const variants = item.variants || [];
// Build title line: generic name (and brand only if not grouped)
let nameLine = `<div class="smart-item-name">${escapeHtml(shoppingName)}`;
if (!isGeneric && item.brand) nameLine += ` <small class="smart-brand">${escapeHtml(item.brand)}</small>`;
nameLine += `</div>`;
// Build subtitle: specific product + brand when grouped, plus any variants
let specificLine = '';
if (isGeneric || variants.length > 0) {
let specifics = [];
specifics.push(item.name + (item.brand ? ` (${item.brand})` : ''));
for (const v of variants) {
specifics.push(v.name + (v.brand ? ` (${v.brand})` : ''));
}
specificLine = `<div class="smart-item-specific">${escapeHtml(specifics.join(' · '))}</div>`;
}
// Stock bar
const pct = Math.min(100, Math.max(0, item.pct_left));
const barColor = pct <= 15 ? '#ef4444' : pct <= 30 ? '#f97316' : pct <= 50 ? '#eab308' : '#22c55e';
@@ -7333,7 +7355,8 @@ function renderSmartItem(item) {
${!item.on_bring ? `<input type="checkbox" class="smart-check" data-idx="${globalIdx}">` : ''}
<span class="smart-item-icon">${catIcon}</span>
<div class="smart-item-info">
<div class="smart-item-name">${escapeHtml(item.name)}${item.brand ? ` <small class="smart-brand">${escapeHtml(item.brand)}</small>` : ''}</div>
${nameLine}
${specificLine}
<div class="smart-item-reasons">${item.reasons.map(r => `<span>${escapeHtml(r)}</span>`).join(' · ')}</div>
<div class="smart-item-badges">
<span class="smart-urgency-badge" style="color:${u.color}">${u.icon} ${u.label}</span>
@@ -7362,9 +7385,15 @@ async function addSmartToBring() {
const idx = parseInt(cb.dataset.idx);
const item = smartShoppingItems[idx];
if (item) {
const shoppingName = item.shopping_name || item.name;
const isGeneric = shoppingName !== item.name;
// When generic, use specific product name + brand as the specification
const spec = isGeneric
? (item.name + (item.brand ? ` · ${item.brand}` : ''))
: _urgencyToSpec(item.urgency, item.brand);
itemsToAdd.push({
name: item.name,
specification: _urgencyToSpec(item.urgency, item.brand),
name: shoppingName,
specification: spec,
});
}
});