feat: migrate existing Bring! items to generic shopping names

- New API action bring_migrate_names: reads current Bring! list, matches
  items against products DB, replaces specific names with shopping_name
  (e.g. 'Mortadella IGP' → 'Affettato' with spec 'Mortadella IGP · Brand')
- New button in Bring! settings: 'Generalizza nomi lista Bring!'
  with live status feedback (migrated / skipped / errors count)
- Auto-refreshes shopping list view after migration
This commit is contained in:
dadaloop82
2026-04-27 17:29:55 +00:00
parent 28a8c938bd
commit 8258591e44
3 changed files with 112 additions and 6 deletions
+82 -6
View File
@@ -254,6 +254,9 @@ try {
case 'bring_clean_specs': case 'bring_clean_specs':
bringCleanSpecs(); bringCleanSpecs();
break; break;
case 'bring_migrate_names':
bringMigrateNames($db);
break;
case 'bring_suggest': case 'bring_suggest':
bringSuggestItems($db); bringSuggestItems($db);
break; break;
@@ -4197,13 +4200,86 @@ function bringCleanSpecs(): void {
} }
/** /**
* Serve smart shopping from cache (written by cron), falling back to live computation. * Migrate existing Bring! list items to use generic shopping names.
* Cache is valid for up to 10 minutes; if stale or missing, compute on the fly. * For each item on the list that matches a product in the DB, if the item name
*/ * is the specific product name (not the generic shopping_name), replace it with
/** * the generic name and move the specific name to the specification field.
* Invalidate the smart shopping cache so the next request recomputes live.
* Call after any inventory_add or inventory_use that changes stock meaningfully.
*/ */
function bringMigrateNames(PDO $db): void {
$auth = bringAuth();
if (!$auth) {
echo json_encode(['success' => false, 'error' => 'Credenziali Bring! non configurate']);
return;
}
$listUUID = $auth['bringListUUID'];
if (empty($listUUID)) {
echo json_encode(['success' => false, 'error' => 'Lista non trovata']);
return;
}
$data = bringRequest('GET', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}");
if (!$data || !isset($data['purchase'])) {
echo json_encode(['success' => false, 'error' => 'Errore nel recupero della lista']);
return;
}
// Build a lookup: product name (lowercase) → [shopping_name, brand]
$products = $db->query("SELECT name, brand, shopping_name FROM products WHERE shopping_name IS NOT NULL AND shopping_name != ''")->fetchAll();
$lookup = [];
foreach ($products as $p) {
$lookup[mb_strtolower($p['name'])] = ['shopping_name' => $p['shopping_name'], 'brand' => $p['brand'] ?? ''];
}
$migrated = 0;
$skipped = 0;
$errors = 0;
foreach ($data['purchase'] as $item) {
$rawName = $item['name'] ?? '';
$itName = bringToItalian($rawName); // translate from Bring internal name if needed
$key = mb_strtolower($itName);
$spec = $item['specification'] ?? '';
if (!isset($lookup[$key])) { $skipped++; continue; }
$shoppingName = $lookup[$key]['shopping_name'];
$brand = $lookup[$key]['brand'];
// Already using the generic name → nothing to do
if (mb_strtolower($rawName) === mb_strtolower($shoppingName)) { $skipped++; continue; }
if (mb_strtolower(bringToItalian($rawName)) === mb_strtolower($shoppingName)) { $skipped++; continue; }
// Build new spec: "Specific Name · Brand" (keep existing spec if already set & different)
$newSpec = $itName . ($brand ? " · {$brand}" : '');
if ($spec !== '' && $spec !== $newSpec && stripos($spec, $itName) === false) {
// There's a custom spec the user wrote — prepend the product name
$newSpec = $itName . ($brand ? " · {$brand}" : '') . ' — ' . $spec;
}
// Step 1: remove old item
$removeBody = http_build_query([
'uuid' => $listUUID,
'purchase' => $rawName,
]);
bringRequest('DELETE', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}/{$rawName}");
// Step 2: add with generic name + spec
$addBody = http_build_query([
'uuid' => $listUUID,
'purchase' => $shoppingName,
'specification' => $newSpec,
]);
$result = bringRequest('PUT', "https://api.getbring.com/rest/v2/bringlists/{$listUUID}", $addBody);
if ($result !== false) {
$migrated++;
} else {
$errors++;
}
}
echo json_encode(['success' => true, 'migrated' => $migrated, 'skipped' => $skipped, 'errors' => $errors]);
}
function invalidateSmartShoppingCache(): void { function invalidateSmartShoppingCache(): void {
$cacheFile = __DIR__ . '/../data/smart_shopping_cache.json'; $cacheFile = __DIR__ . '/../data/smart_shopping_cache.json';
if (file_exists($cacheFile)) { if (file_exists($cacheFile)) {
+24
View File
@@ -7416,6 +7416,30 @@ function renderSmartItem(item) {
</div>`; </div>`;
} }
async function migrateBringNames(btn) {
const statusEl = document.getElementById('bring-migrate-status');
if (btn) btn.disabled = true;
if (statusEl) { statusEl.style.display = 'inline'; statusEl.textContent = '⏳ In corso…'; }
try {
const data = await api('bring_migrate_names', {}, 'POST', {});
if (data.success) {
const msg = `${data.migrated} aggiornati, ${data.skipped} già ok${data.errors ? `, ${data.errors} errori` : ''}`;
if (statusEl) statusEl.textContent = msg;
if (data.migrated > 0) {
showToast(`🔄 ${data.migrated} nomi generalizzati in Bring!`, 'success');
loadShoppingList(); // refresh the shopping list view
} else {
showToast('Tutti i nomi sono già aggiornati', 'info');
}
} else {
if (statusEl) statusEl.textContent = '❌ ' + (data.error || 'Errore');
}
} catch(e) {
if (statusEl) statusEl.textContent = '❌ Errore di connessione';
}
if (btn) btn.disabled = false;
}
async function addSmartToBring() { async function addSmartToBring() {
const checks = document.querySelectorAll('.smart-check:checked'); const checks = document.querySelectorAll('.smart-check:checked');
if (checks.length === 0) { if (checks.length === 0) {
+6
View File
@@ -735,6 +735,12 @@
<input type="password" id="setting-bring-password" class="form-input" placeholder="Password"> <input type="password" id="setting-bring-password" class="form-input" placeholder="Password">
<button class="btn btn-small btn-secondary mt-2" onclick="togglePasswordVisibility('setting-bring-password')">👁️ Mostra/Nascondi</button> <button class="btn btn-small btn-secondary mt-2" onclick="togglePasswordVisibility('setting-bring-password')">👁️ Mostra/Nascondi</button>
</div> </div>
<div class="form-group" style="margin-top:16px;padding-top:14px;border-top:1px solid var(--border)">
<label>🔄 Aggiorna nomi nella lista</label>
<p style="font-size:0.82rem;color:var(--text-muted);margin:4px 0 10px">Sostituisce i nomi specifici con quelli generici (es. "Mortadella IGP" → "Affettato") negli item già presenti in Bring!.</p>
<button class="btn btn-secondary btn-small" onclick="migrateBringNames(this)">🔄 Generalizza nomi lista Bring!</button>
<span id="bring-migrate-status" style="display:none;margin-left:8px;font-size:0.85rem"></span>
</div>
</div> </div>
</div> </div>
<!-- Recipe Tab --> <!-- Recipe Tab -->