diff --git a/api/index.php b/api/index.php
index e66016c..a94af66 100644
--- a/api/index.php
+++ b/api/index.php
@@ -135,6 +135,10 @@ try {
getClientLog();
break;
+ case 'migrate_units':
+ migrateUnitsToBase($db);
+ break;
+
// ===== SPESA ONLINE =====
case 'dupliclick_login':
dupliclickLogin();
@@ -1480,7 +1484,7 @@ REGOLE IMPORTANTI:
5. Adatta le quantità per $persons persona/e
6. Se non ci sono abbastanza ingredienti per una ricetta completa, suggerisci la migliore combinazione possibile
7. La ricetta deve essere adatta al pasto: $mealLabel
-8. IMPORTANTE - QUANTITÀ NUMERICHE: per ogni ingrediente dalla dispensa, il campo "qty_number" DEVE contenere il valore NUMERICO da scalare dall'inventario, espresso nella STESSA unità di misura della dispensa. Esempio: se in dispensa c'è "Farina: 1000 g" e la ricetta richiede 200g, qty_number = 200. Se "Riso: 2 kg" e servono 300g, qty_number = 0.3. Per ingredienti non dalla dispensa, qty_number = 0.
+8. IMPORTANTE - QUANTITÀ NUMERICHE: per ogni ingrediente dalla dispensa, il campo "qty_number" DEVE contenere il valore NUMERICO da scalare dall'inventario, espresso nella STESSA unità di misura della dispensa. Le unità ammesse sono SOLO: g (grammi), ml (millilitri), pz (pezzi), conf (confezioni). NON usare mai kg o litri. Esempio: se in dispensa c'è "Farina: 1000 g" e la ricetta richiede 200g, qty_number = 200. Se "Riso: 2000 g" e servono 300g, qty_number = 300. Per ingredienti non dalla dispensa, qty_number = 0.
9. GESTIONE SMART QUANTITÀ: NON lasciare rimasugli poco usabili in dispensa. Se un ingrediente ha una quantità piccola (es. 50g di formaggio, 1 uovo, 100ml di latte), preferisci usarlo TUTTO piuttosto che lasciarne una quantità inutilizzabile. Se invece la quantità è abbondante, usa solo il necessario lasciando abbastanza per un altro pasto. Pensa sempre: "quello che resta sarà sufficiente per un altro utilizzo?"
INGREDIENTI DISPONIBILI IN DISPENSA:
@@ -1622,27 +1626,27 @@ PROMPT;
$recipeVal = (float)str_replace(',', '.', $qm[1]);
$ru = strtolower($qm[2]);
if (strpos($ru, 'g') === 0) $recipeUnit = 'g';
- elseif ($ru === 'kg') $recipeUnit = 'kg';
+ elseif ($ru === 'kg') { $recipeUnit = 'g'; $recipeVal *= 1000; }
elseif ($ru === 'ml') $recipeUnit = 'ml';
- elseif ($ru === 'cl') $recipeUnit = 'ml'; // cl→ml
- elseif ($ru === 'l' || strpos($ru, 'litr') === 0) $recipeUnit = 'l';
+ elseif ($ru === 'cl') { $recipeUnit = 'ml'; $recipeVal *= 10; }
+ elseif ($ru === 'l' || strpos($ru, 'litr') === 0) { $recipeUnit = 'ml'; $recipeVal *= 1000; }
elseif (strpos($ru, 'pz') === 0 || strpos($ru, 'pezz') === 0) $recipeUnit = 'pz';
elseif (strpos($ru, 'conf') === 0) $recipeUnit = 'conf';
}
// Convert qty_number to inventory unit if mismatch detected
if ($recipeUnit && $recipeUnit !== $invUnit) {
- // Weight conversions
+ // Weight conversions (both should be 'g' now, but handle legacy 'kg')
if ($recipeUnit === 'g' && $invUnit === 'kg') {
$qtyNum = $recipeVal / 1000;
- } elseif ($recipeUnit === 'kg' && $invUnit === 'g') {
- $qtyNum = $recipeVal * 1000;
- // Volume conversions
+ } elseif ($recipeUnit === 'g' && $invUnit === 'g') {
+ $qtyNum = $recipeVal;
+ // Volume conversions (both should be 'ml' now, but handle legacy 'l')
} elseif ($recipeUnit === 'ml' && $invUnit === 'l') {
$qtyNum = $recipeVal / 1000;
- } elseif ($recipeUnit === 'l' && $invUnit === 'ml') {
- $qtyNum = $recipeVal * 1000;
- // g/kg/ml/l → pz (approximate to nearest piece)
+ } elseif ($recipeUnit === 'ml' && $invUnit === 'ml') {
+ $qtyNum = $recipeVal;
+ // g/ml → pz (approximate to nearest piece)
} elseif ($invUnit === 'pz' || $invUnit === 'conf') {
$defQty = (float)($bestMatch['default_quantity'] ?? 0);
if ($defQty > 0) {
@@ -3123,3 +3127,51 @@ function chatClear(PDO $db): void {
$db->exec("DELETE FROM chat_messages");
echo json_encode(['success' => true]);
}
+
+/**
+ * One-time migration: convert all kg→g and l→ml in products table,
+ * and scale inventory quantities accordingly.
+ */
+function migrateUnitsToBase(PDO $db): void {
+ $changes = 0;
+
+ // Get products with kg or l units
+ $stmt = $db->query("SELECT id, unit, default_quantity, package_unit FROM products WHERE unit IN ('kg','l') OR package_unit IN ('kg','l')");
+ $products = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ foreach ($products as $p) {
+ $newUnit = $p['unit'];
+ $newDefQty = (float)$p['default_quantity'];
+ $newPkgUnit = $p['package_unit'];
+ $scaleInventory = false;
+
+ if ($p['unit'] === 'kg') {
+ $newUnit = 'g';
+ $newDefQty = $newDefQty * 1000;
+ $scaleInventory = true;
+ } elseif ($p['unit'] === 'l') {
+ $newUnit = 'ml';
+ $newDefQty = $newDefQty * 1000;
+ $scaleInventory = true;
+ }
+
+ if ($p['package_unit'] === 'kg') {
+ $newPkgUnit = 'g';
+ if ($p['unit'] === 'conf') $newDefQty = $newDefQty * 1000;
+ } elseif ($p['package_unit'] === 'l') {
+ $newPkgUnit = 'ml';
+ if ($p['unit'] === 'conf') $newDefQty = $newDefQty * 1000;
+ }
+
+ $upd = $db->prepare("UPDATE products SET unit = ?, default_quantity = ?, package_unit = ? WHERE id = ?");
+ $upd->execute([$newUnit, $newDefQty, $newPkgUnit, $p['id']]);
+ $changes++;
+
+ // Scale inventory quantities (kg→g means multiply by 1000)
+ if ($scaleInventory) {
+ $db->prepare("UPDATE inventory SET quantity = quantity * 1000 WHERE product_id = ?")->execute([$p['id']]);
+ }
+ }
+
+ echo json_encode(['success' => true, 'changes' => $changes]);
+}
diff --git a/assets/js/app.js b/assets/js/app.js
index c71c4ee..adc2011 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -267,6 +267,8 @@ function detectUnitAndQuantity(quantityInfo) {
let perUnitVal = parseFloat(multiMatch[2].replace(',', '.'));
let perUnitUnit = multiMatch[3].toLowerCase();
if (perUnitUnit === 'cl') { perUnitUnit = 'ml'; perUnitVal *= 10; }
+ if (perUnitUnit === 'kg') { perUnitUnit = 'g'; perUnitVal *= 1000; }
+ if (perUnitUnit === 'l') { perUnitUnit = 'ml'; perUnitVal *= 1000; }
return { unit: 'conf', quantity: perUnitVal, packageUnit: perUnitUnit, confCount: count, weightInfo: quantityInfo };
}
// Match single package patterns like "500 g", "1 l", "750 ml", "1.5 kg"
@@ -275,6 +277,8 @@ function detectUnitAndQuantity(quantityInfo) {
let unit = match[2].toLowerCase();
let val = parseFloat(match[1].replace(',', '.'));
if (unit === 'cl') { unit = 'ml'; val *= 10; }
+ if (unit === 'kg') { unit = 'g'; val *= 1000; }
+ if (unit === 'l') { unit = 'ml'; val *= 1000; }
return { unit, quantity: val, weightInfo: quantityInfo };
}
return { unit: 'pz', quantity: 1, weightInfo: quantityInfo };
@@ -990,7 +994,7 @@ async function loadDashboard() {
const locInfo = LOCATIONS[item.location] || { icon: '📦', label: item.location };
const qty = parseFloat(item.quantity);
const pkgSize = parseFloat(item.default_quantity);
- const unitLabels = { 'ml': 'ml', 'l': 'L', 'g': 'g', 'kg': 'kg', 'pz': 'pz' };
+ const unitLabels = { 'ml': 'ml', 'g': 'g', 'pz': 'pz' };
let qtyText = '';
if (item.unit === 'conf') {
@@ -1061,9 +1065,7 @@ const QTY_THRESHOLDS = {
'pz': { min: 0.3, max: 50 },
'conf': { min: 0.3, max: 50 },
'g': { min: 3, max: 10000 },
- 'kg': { min: 0.005, max: 50 },
'ml': { min: 3, max: 10000 },
- 'l': { min: 0.005, max: 50 },
};
function isSuspiciousQty(qty, unit) {
@@ -1252,9 +1254,7 @@ function showAlertItemDetail(inventoryId, productId) {
}
function formatSubRemainder(amt, pkgUnit) {
- const uL = { 'kg': 'kg', 'g': 'g', 'l': 'L', 'ml': 'ml' };
- if (pkgUnit === 'l' && amt < 1) return `${Math.round(amt * 1000)}ml`;
- if (pkgUnit === 'kg' && amt < 1) return `${Math.round(amt * 1000)}g`;
+ const uL = { 'g': 'g', 'ml': 'ml' };
if (pkgUnit === 'ml' || pkgUnit === 'g') return `${Math.round(amt)}${uL[pkgUnit] || pkgUnit}`;
return `${Math.round(amt * 10) / 10}${uL[pkgUnit] || pkgUnit}`;
}
@@ -1262,7 +1262,7 @@ function formatSubRemainder(amt, pkgUnit) {
function formatQuantity(qty, unit, defaultQty, packageUnit) {
if (!qty && qty !== 0) return '';
const n = parseFloat(qty);
- const unitLabels = { 'pz': 'pz', 'kg': 'kg', 'g': 'g', 'l': 'L', 'ml': 'ml', 'conf': 'conf' };
+ const unitLabels = { 'pz': 'pz', 'g': 'g', 'ml': 'ml', 'conf': 'conf' };
const label = unitLabels[unit] || unit || 'pz';
// Special handling for conf with partial packages
@@ -1292,7 +1292,7 @@ function formatQuantity(qty, unit, defaultQty, packageUnit) {
// Returns { mainQty: '10', unitLabel: 'conf', packageDetail: 'da 36g', fraction: '¼' }
function formatQuantityParts(qty, unit, defaultQty, packageUnit) {
const n = parseFloat(qty) || 0;
- const unitLabels = { 'pz': 'pz', 'kg': 'kg', 'g': 'g', 'l': 'L', 'ml': 'ml', 'conf': 'conf' };
+ const unitLabels = { 'pz': 'pz', 'g': 'g', 'ml': 'ml', 'conf': 'conf' };
const label = unitLabels[unit] || unit || 'pz';
// Special handling for conf with partial packages
@@ -1601,7 +1601,7 @@ function editInventoryItem(id) {
@@ -2423,12 +2423,12 @@ function onCategoryChange(fromAutoDetect = false) {
'latticini': { unit: 'pz', qty: 1 },
'carne': { unit: 'g', qty: 500 },
'pesce': { unit: 'g', qty: 300 },
- 'frutta': { unit: 'kg', qty: 1 },
- 'verdura': { unit: 'kg', qty: 0.5 },
+ 'frutta': { unit: 'g', qty: 1000 },
+ 'verdura': { unit: 'g', qty: 500 },
'pasta': { unit: 'g', qty: 500 },
'pane': { unit: 'pz', qty: 1 },
'surgelati': { unit: 'g', qty: 450 },
- 'bevande': { unit: 'l', qty: 1 },
+ 'bevande': { unit: 'ml', qty: 1000 },
'condimenti': { unit: 'pz', qty: 1 },
'snack': { unit: 'g', qty: 250 },
'conserve': { unit: 'g', qty: 400 },
@@ -2824,7 +2824,7 @@ function editActionInventoryItem(inventoryId) {
@@ -3270,9 +3270,7 @@ function onAddUnitChange() {
// Convert between related units if logical
if (unit === 'g' && currentQty <= 10) qtyInput.value = currentProduct.weight_info ? parseFloat(currentProduct.weight_info) || 250 : 250;
- if (unit === 'kg' && currentQty > 100) qtyInput.value = (currentQty / 1000).toFixed(1);
if (unit === 'ml' && currentQty <= 10) qtyInput.value = 500;
- if (unit === 'l' && currentQty > 100) qtyInput.value = (currentQty / 1000).toFixed(1);
if (unit === 'pz' && currentQty > 100) qtyInput.value = 1;
if (unit === 'conf' && currentQty > 10) qtyInput.value = 1;
}
@@ -3283,8 +3281,6 @@ function updateAddQtyStep() {
qtyInput.step = 'any';
if (unit === 'g' || unit === 'ml') {
qtyInput.min = '1';
- } else if (unit === 'kg' || unit === 'l') {
- qtyInput.min = '0.1';
} else {
qtyInput.min = '1';
}
@@ -3300,9 +3296,7 @@ function adjustAddQty(delta) {
const unit = document.getElementById('add-unit').value;
let val = parseFloat(qtyInput.value) || 0;
let step;
- if (unit === 'kg' || unit === 'l') {
- step = val < 1 ? 0.1 : 0.5;
- } else if (unit === 'g' || unit === 'ml') {
+ if (unit === 'g' || unit === 'ml') {
step = val < 50 ? 1 : (val < 500 ? 10 : 50);
} else {
step = 1;
@@ -3431,7 +3425,7 @@ async function submitAdd(e) {
let qtyInfo = '';
if (result.total_qty) {
const u = result.unit || 'pz';
- const unitLabels = { 'pz': 'pz', 'kg': 'kg', 'g': 'g', 'l': 'L', 'ml': 'ml', 'conf': 'conf' };
+ const unitLabels = { 'pz': 'pz', 'g': 'g', 'ml': 'ml', 'conf': 'conf' };
const uLabel = unitLabels[u] || u;
if (u === 'conf' && result.package_unit && result.default_quantity > 0) {
const pkgLabel = unitLabels[result.package_unit] || result.package_unit;
@@ -3536,7 +3530,7 @@ async function loadUseInventoryInfo() {
// --- CONF MODE: show sub-unit controls ---
const totalConf = items.reduce((s, i) => s + parseFloat(i.quantity), 0);
const totalSub = totalConf * pkgSize;
- const unitLabels = { 'ml': 'ml', 'l': 'L', 'g': 'g', 'kg': 'kg', 'pz': 'pz' };
+ const unitLabels = { 'ml': 'ml', 'g': 'g', 'pz': 'pz' };
const subLabel = unitLabels[pkgUnit] || pkgUnit;
_useConfMode = { packageSize: pkgSize, packageUnit: pkgUnit, totalSub, totalConf, subLabel };
@@ -3568,9 +3562,9 @@ async function loadUseInventoryInfo() {
}).join(' · ');
const qtyInput = document.getElementById('use-quantity');
- qtyInput.value = (unit === 'kg' || unit === 'l') ? 0.1 : 1;
+ qtyInput.value = 1;
qtyInput.step = 'any';
- qtyInput.min = (unit === 'kg' || unit === 'l') ? '0.01' : (unit === 'g' || unit === 'ml') ? '1' : '1';
+ qtyInput.min = (unit === 'g' || unit === 'ml') ? '1' : '1';
document.getElementById('use-partial-hint').textContent = 'Oppure specifica la quantità usata:';
}
} catch(e) {
@@ -3607,9 +3601,7 @@ function switchUseUnit(mode) {
function getSubUnitStep(pkgUnit) {
switch (pkgUnit) {
case 'ml': return 50;
- case 'l': return 0.1;
case 'g': return 10;
- case 'kg': return 0.05;
default: return 1;
}
}
@@ -3625,16 +3617,13 @@ function adjustUseQty(direction) {
} else {
// Unit-aware step for normal mode
const u = _useNormalUnit || 'pz';
- if (u === 'kg' || u === 'l') {
- step = val <= 0.1 ? 0.01 : (val < 1 ? 0.1 : 0.5);
- } else if (u === 'g' || u === 'ml') {
+ if (u === 'g' || u === 'ml') {
step = val < 50 ? 1 : (val < 500 ? 10 : 50);
} else {
step = 1;
}
}
- const minVal = ((_useNormalUnit === 'kg' || _useNormalUnit === 'l') && !_useConfMode) ? 0.01 : step;
- val = Math.max(minVal, val + direction * step);
+ val = Math.max(step, val + direction * step);
input.value = Math.round(val * 1000) / 1000;
}
@@ -3653,7 +3642,6 @@ function isLowStock(totalRemaining, unit, defaultQty) {
if (defaultQty > 0) return totalRemaining <= defaultQty * 0.25;
// Fallback fixed thresholds
if (unit === 'g' || unit === 'ml') return totalRemaining <= 100;
- if (unit === 'kg' || unit === 'l') return totalRemaining <= 0.15;
return false;
}
@@ -3700,7 +3688,7 @@ function showLowStockBringPrompt(result, afterCallback) {
const subTotal = Math.round(totalRemaining * defaultQty);
remainLabel = `${subTotal}${result.product_package_unit}`;
} else {
- const unitLabels = { pz: 'pz', g: 'g', kg: 'kg', ml: 'ml', l: 'L', conf: 'conf' };
+ const unitLabels = { pz: 'pz', g: 'g', ml: 'ml', conf: 'conf' };
remainLabel = `${Number.isInteger(totalRemaining) ? totalRemaining : totalRemaining.toFixed(1)} ${unitLabels[unit] || unit}`;
}
@@ -4481,10 +4469,10 @@ function parseQtyFromSpec(spec) {
let val = parseFloat(m[1].replace(',', '.'));
const unit = m[2].toLowerCase();
if (unit === 'g' || unit === 'gr') return { kg: val / 1000, label: val + 'g', type: 'weight' };
- if (unit === 'kg') return { kg: val, label: val + 'kg', type: 'weight' };
+ if (unit === 'kg') return { kg: val, label: (val * 1000) + 'g', type: 'weight' };
if (unit === 'ml') return { kg: val / 1000, label: val + 'ml', type: 'weight' };
if (unit === 'cl') return { kg: val / 100, label: val * 10 + 'ml', type: 'weight' };
- if (unit === 'l' || unit === 'lt') return { kg: val, label: val + 'L', type: 'weight' };
+ if (unit === 'l' || unit === 'lt') return { kg: val, label: (val * 1000) + 'ml', type: 'weight' };
}
// Match unit count: 2 pz, 3 pezzi, 5, 2x, ~5 pz
const pzMatch = s.match(/~?(\d+)\s*(pz|pezzi|x|$)/i);
@@ -5792,7 +5780,7 @@ async function useRecipeIngredient(idx, productId, location, qtyNumber, btn) {
if (isConf) {
const totalConf = items.reduce((s, i) => s + parseFloat(i.quantity), 0);
const totalSub = totalConf * pkgSize;
- const unitLabels = { 'ml': 'ml', 'l': 'L', 'g': 'g', 'kg': 'kg', 'pz': 'pz' };
+ const unitLabels = { 'ml': 'ml', 'g': 'g', 'pz': 'pz' };
const subLabel = unitLabels[pkgUnit] || pkgUnit;
_recipeUseConfMode = { packageSize: pkgSize, packageUnit: pkgUnit, totalSub, totalConf, subLabel, _activeUnit: 'sub' };
@@ -5813,9 +5801,9 @@ async function useRecipeIngredient(idx, productId, location, qtyNumber, btn) {
`;
} else {
_recipeUseNormalUnit = unit;
- const unitLabels = { 'pz': 'pz', 'kg': 'kg', 'g': 'g', 'l': 'L', 'ml': 'ml' };
+ const unitLabels = { 'pz': 'pz', 'g': 'g', 'ml': 'ml' };
const unitLabel = unitLabels[unit] || unit;
- const inputMin = (unit === 'kg' || unit === 'l') ? '0.01' : '0.1';
+ const inputMin = '0.1';
qtySection = `
Quantità da usare (${unitLabel}):
@@ -5907,16 +5895,13 @@ function adjustRecipeUseQty(direction) {
step = 0.5;
} else {
const u = _recipeUseNormalUnit || 'pz';
- if (u === 'kg' || u === 'l') {
- step = val <= 0.1 ? 0.01 : (val < 1 ? 0.1 : 0.5);
- } else if (u === 'g' || u === 'ml') {
+ if (u === 'g' || u === 'ml') {
step = val < 50 ? 1 : (val < 500 ? 10 : 50);
} else {
step = 1;
}
}
- const minVal = ((_recipeUseNormalUnit === 'kg' || _recipeUseNormalUnit === 'l') && !_recipeUseConfMode) ? 0.01 : step;
- val = Math.max(minVal, val + direction * step);
+ val = Math.max(step, val + direction * step);
input.value = Math.round(val * 1000) / 1000;
}
diff --git a/data/dispensa.db b/data/dispensa.db
index 4d1f06e..3c6da90 100644
Binary files a/data/dispensa.db and b/data/dispensa.db differ
diff --git a/index.html b/index.html
index 3cae775..2079753 100644
--- a/index.html
+++ b/index.html
@@ -218,9 +218,7 @@
-
-
@@ -229,9 +227,7 @@
@@ -440,9 +436,7 @@
@@ -458,9 +452,7 @@
@@ -997,6 +989,6 @@
-
+