Bring! catalogo IT↔DE: nomi riconosciuti con icone e categorie

This commit is contained in:
dadaloop82
2026-03-10 13:46:40 +00:00
parent 81b30f61a2
commit 6e673963c9
3 changed files with 121 additions and 11 deletions
+1
View File
@@ -7,6 +7,7 @@ data/*.db-shm
# Bring! auth token cache # Bring! auth token cache
data/bring_token.json data/bring_token.json
data/bring_catalog.json
# SSL CA cert (local only) # SSL CA cert (local only)
ca.crt ca.crt
+109 -6
View File
@@ -1078,6 +1078,76 @@ function bringRequest(string $method, string $url, ?string $body = null): ?array
return $data ?? ['_raw' => $response]; return $data ?? ['_raw' => $response];
} }
/**
* Load and cache the Bring! IT↔DE catalog mapping.
* Returns ['de2it' => [German => Italian], 'it2de' => [italian_lower => German]]
*/
function bringCatalog(): array {
$cacheFile = __DIR__ . '/../data/bring_catalog.json';
// Cache for 24 hours
if (file_exists($cacheFile) && filemtime($cacheFile) > time() - 86400) {
return json_decode(file_get_contents($cacheFile), true) ?: ['de2it' => [], 'it2de' => []];
}
$json = @file_get_contents('https://web.getbring.com/locale/articles.it-IT.json');
if (!$json) return ['de2it' => [], 'it2de' => []];
$data = json_decode($json, true);
if (!$data) return ['de2it' => [], 'it2de' => []];
$de2it = [];
$it2de = [];
foreach ($data as $deKey => $itVal) {
if (!is_string($itVal) || empty($itVal)) continue;
$de2it[$deKey] = $itVal;
$it2de[mb_strtolower($itVal)] = $deKey;
}
$catalog = ['de2it' => $de2it, 'it2de' => $it2de];
@file_put_contents($cacheFile, json_encode($catalog, JSON_UNESCAPED_UNICODE));
return $catalog;
}
/** Translate a Bring! item name from German key to Italian display name */
function bringToItalian(string $name): string {
$catalog = bringCatalog();
return $catalog['de2it'][$name] ?? $name;
}
/** Translate an Italian product name to the Bring! German catalog key (fuzzy match) */
function italianToBring(string $italianName): string {
$catalog = bringCatalog();
$lower = mb_strtolower(trim($italianName));
// Exact match
if (isset($catalog['it2de'][$lower])) {
return $catalog['it2de'][$lower];
}
// Try partial match: "Spinaci freschi" → match "Spinaci"
foreach ($catalog['it2de'] as $itLower => $deKey) {
if (str_contains($lower, $itLower) || str_contains($itLower, $lower)) {
return $deKey;
}
}
// Try matching first word: "Petto di pollo" → "Pollo" = Poulet
$words = explode(' ', $lower);
foreach ($words as $word) {
if (mb_strlen($word) < 3) continue;
foreach ($catalog['it2de'] as $itLower => $deKey) {
if ($itLower === $word) {
return $deKey;
}
}
}
// No match - return original (Bring! will show as custom item)
return $italianName;
}
function bringGetList(): void { function bringGetList(): void {
$auth = bringAuth(); $auth = bringAuth();
if (!$auth) { if (!$auth) {
@@ -1108,16 +1178,20 @@ function bringGetList(): void {
if (isset($data['purchase'])) { if (isset($data['purchase'])) {
foreach ($data['purchase'] as $item) { foreach ($data['purchase'] as $item) {
$rawName = $item['name'] ?? '';
$purchase[] = [ $purchase[] = [
'name' => $item['name'] ?? '', 'name' => bringToItalian($rawName),
'rawName' => $rawName,
'specification' => $item['specification'] ?? '', 'specification' => $item['specification'] ?? '',
]; ];
} }
} }
if (isset($data['recently'])) { if (isset($data['recently'])) {
foreach ($data['recently'] as $item) { foreach ($data['recently'] as $item) {
$rawName = $item['name'] ?? '';
$recently[] = [ $recently[] = [
'name' => $item['name'] ?? '', 'name' => bringToItalian($rawName),
'rawName' => $rawName,
'specification' => $item['specification'] ?? '', 'specification' => $item['specification'] ?? '',
]; ];
} }
@@ -1128,7 +1202,7 @@ function bringGetList(): void {
'listUUID' => $listUUID, 'listUUID' => $listUUID,
'purchase' => $purchase, 'purchase' => $purchase,
'recently' => $recently, 'recently' => $recently,
]); ], JSON_UNESCAPED_UNICODE);
} }
function bringAddItems(): void { function bringAddItems(): void {
@@ -1155,9 +1229,12 @@ function bringAddItems(): void {
$spec = $item['specification'] ?? ''; $spec = $item['specification'] ?? '';
if (empty($name)) continue; if (empty($name)) continue;
// Map Italian name to Bring! catalog key (German) for proper recognition
$bringName = italianToBring($name);
$body = http_build_query([ $body = http_build_query([
'uuid' => $listUUID, 'uuid' => $listUUID,
'purchase' => $name, 'purchase' => $bringName,
'specification' => $spec, 'specification' => $spec,
]); ]);
@@ -1188,9 +1265,13 @@ function bringRemoveItem(): void {
return; return;
} }
// Use rawName (German key) if provided, otherwise try to map
$rawName = $input['rawName'] ?? '';
$removeName = !empty($rawName) ? $rawName : italianToBring($name);
$body = http_build_query([ $body = http_build_query([
'uuid' => $listUUID, 'uuid' => $listUUID,
'remove' => $name, 'remove' => $removeName,
]); ]);
$result = bringRequest('PUT', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}", $body); $result = bringRequest('PUT', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}", $body);
@@ -1222,7 +1303,8 @@ function bringSuggestItems(PDO $db): void {
$data = bringRequest('GET', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}"); $data = bringRequest('GET', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}");
if ($data && isset($data['purchase'])) { if ($data && isset($data['purchase'])) {
foreach ($data['purchase'] as $item) { foreach ($data['purchase'] as $item) {
$bringItems[] = $item['name'] ?? ''; $rawName = $item['name'] ?? '';
$bringItems[] = bringToItalian($rawName);
} }
} }
} }
@@ -1287,6 +1369,17 @@ function bringSuggestItems(PDO $db): void {
$meseIt = $mesi_it[date('F')] ?? date('F'); $meseIt = $mesi_it[date('F')] ?? date('F');
$anno = date('Y'); $anno = date('Y');
// Get catalog Italian names for AI to use
$catalog = bringCatalog();
$catalogNames = array_values($catalog['de2it']);
// Filter only food-related items (exclude categories and non-food)
$catalogNames = array_filter($catalogNames, function($n) {
$skip = ['Fai da te', 'Giardino', 'Atrezzi', 'Annaffiatoio', 'Rasaerba', 'Sementi', 'Propangas', 'Vernice', 'Pennello', 'Viti', 'Chiodi', 'Barbecue', 'Ombrellone', 'Terriccio', 'Concime', 'Articoli propri', 'Usati di recente'];
foreach ($skip as $s) { if (str_contains($n, $s)) return false; }
return mb_strlen($n) > 1;
});
$catalogList = implode(', ', array_slice($catalogNames, 0, 200));
$prompt = <<<PROMPT $prompt = <<<PROMPT
Sei un nutrizionista e consulente per la spesa domestica italiano. Il tuo obiettivo è aiutare l'utente a fare una spesa SANA, EQUILIBRATA e INTELLIGENTE. Sei un nutrizionista e consulente per la spesa domestica italiano. Il tuo obiettivo è aiutare l'utente a fare una spesa SANA, EQUILIBRATA e INTELLIGENTE.
@@ -1298,9 +1391,19 @@ DATA ATTUALE: {$meseIt} {$anno}
=== LISTA BRING! (già pianificato per la spesa) === === LISTA BRING! (già pianificato per la spesa) ===
{$bringText} {$bringText}
=== CATALOGO PRODOTTI RICONOSCIUTI ===
Usa ESATTAMENTE questi nomi quando possibile (sono i nomi che il sistema riconosce con icone e categorie):
{$catalogList}
=== IL TUO COMPITO === === IL TUO COMPITO ===
Analizza attentamente l'inventario dell'utente e suggerisci cosa MANCA per una settimana di alimentazione sana. Analizza attentamente l'inventario dell'utente e suggerisci cosa MANCA per una settimana di alimentazione sana.
REGOLA FONDAMENTALE SUI NOMI:
- Il campo "name" DEVE essere uno dei nomi dal CATALOGO PRODOTTI sopra, scritto ESATTAMENTE come appare.
- Esempio: usa "Spinaci" (non "Spinaci freschi"), "Pollo" (non "Petto di pollo"), "Mele" (non "Mele Golden").
- Se vuoi specificare la variante, mettila nel campo "specification" (es: name="Pollo", specification="petto, 500g").
- Se il prodotto non è nel catalogo, usa il nome più generico possibile in italiano.
RAGIONA COSÌ: RAGIONA COSÌ:
1. CONTROLLA cosa ha già: guarda OGNI prodotto nell'inventario prima di suggerire. Se ha già pollo, non suggerire pollo. Se ha già pasta, non suggerire altra pasta. 1. CONTROLLA cosa ha già: guarda OGNI prodotto nell'inventario prima di suggerire. Se ha già pollo, non suggerire pollo. Se ha già pasta, non suggerire altra pasta.
2. CONTROLLA la lista Bring!: NON suggerire nulla che sia già nella lista. Neanche varianti simili (es. "Fagioli" se c'è "Fagioli in lattina"). 2. CONTROLLA la lista Bring!: NON suggerire nulla che sia già nella lista. Neanche varianti simili (es. "Fagioli" se c'è "Fagioli in lattina").
+11 -5
View File
@@ -2224,7 +2224,7 @@ function renderShoppingItems() {
return; return;
} }
container.innerHTML = shoppingItems.map(item => { container.innerHTML = shoppingItems.map((item, idx) => {
const catIcon = CATEGORY_ICONS[guessCategoryFromName(item.name)] || '🛒'; const catIcon = CATEGORY_ICONS[guessCategoryFromName(item.name)] || '🛒';
return ` return `
<div class="shopping-item"> <div class="shopping-item">
@@ -2233,16 +2233,22 @@ function renderShoppingItems() {
<div class="shopping-item-name">${escapeHtml(item.name)}</div> <div class="shopping-item-name">${escapeHtml(item.name)}</div>
${item.specification ? `<div class="shopping-item-spec">${escapeHtml(item.specification)}</div>` : ''} ${item.specification ? `<div class="shopping-item-spec">${escapeHtml(item.specification)}</div>` : ''}
</div> </div>
<button class="shopping-item-remove" onclick="removeBringItem('${escapeHtml(item.name)}')" title="Rimuovi">✕</button> <button class="shopping-item-remove" onclick="removeBringItem(${idx})" title="Rimuovi">✕</button>
</div>`; </div>`;
}).join(''); }).join('');
} }
async function removeBringItem(name) { async function removeBringItem(idx) {
const item = shoppingItems[idx];
if (!item) return;
try { try {
const data = await api('bring_remove', {}, 'POST', { name, listUUID: shoppingListUUID }); const data = await api('bring_remove', {}, 'POST', {
name: item.name,
rawName: item.rawName || '',
listUUID: shoppingListUUID
});
if (data.success) { if (data.success) {
shoppingItems = shoppingItems.filter(i => i.name !== name); shoppingItems.splice(idx, 1);
renderShoppingItems(); renderShoppingItems();
showToast('Rimosso dalla lista', 'success'); showToast('Rimosso dalla lista', 'success');
} }