Fix conversione unità ricette + indicatore confezione
- Fix qty_number nelle ricette: validazione e conversione automatica delle unità di misura (g↔kg, ml↔L, g→pz con default_quantity) - Sanity check: cap qty_number al disponibile, correzione valori assurdamente piccoli dovuti a errori di Gemini - Aggiunto indicatore confezione (¼, ½, ¾, pieno) vicino alla quantità nell'inventario e nella barra stato dopo scansione - Aggiunto default_quantity nella query inventory_list - Nuova funzione formatPackageFraction() per calcolo frazioni
This commit is contained in:
+65
-1
@@ -365,7 +365,7 @@ function searchProducts(PDO $db): void {
|
||||
function listInventory(PDO $db): void {
|
||||
$location = $_GET['location'] ?? '';
|
||||
$query = "
|
||||
SELECT i.*, p.name, p.brand, p.category, p.image_url, p.unit, p.barcode
|
||||
SELECT i.*, p.name, p.brand, p.category, p.image_url, p.unit, p.barcode, p.default_quantity
|
||||
FROM inventory i
|
||||
JOIN products p ON i.product_id = p.id
|
||||
";
|
||||
@@ -1042,6 +1042,9 @@ PROMPT;
|
||||
if ($bestMatch && $bestScore > 30) {
|
||||
$ing['product_id'] = (int)$bestMatch['product_id'];
|
||||
$ing['location'] = $bestMatch['location'];
|
||||
$ing['inventory_unit'] = $bestMatch['unit'];
|
||||
$ing['inventory_qty'] = (float)$bestMatch['quantity'];
|
||||
$ing['default_quantity'] = (float)($bestMatch['default_quantity'] ?? 0);
|
||||
$ing['available_qty'] = $bestMatch['quantity'] . ' ' . $bestMatch['unit'];
|
||||
if (!empty($bestMatch['brand'])) {
|
||||
$ing['brand'] = $bestMatch['brand'];
|
||||
@@ -1049,6 +1052,67 @@ PROMPT;
|
||||
if (!empty($bestMatch['expiry_date'])) {
|
||||
$ing['expiry_date'] = $bestMatch['expiry_date'];
|
||||
}
|
||||
|
||||
// === FIX qty_number: validate and convert units ===
|
||||
$qtyNum = (float)($ing['qty_number'] ?? 0);
|
||||
$invUnit = $bestMatch['unit'] ?? 'pz';
|
||||
$invQty = (float)$bestMatch['quantity'];
|
||||
|
||||
if ($qtyNum > 0) {
|
||||
// Parse the recipe qty string to detect what unit Gemini intended
|
||||
$recipeQty = $ing['qty'] ?? '';
|
||||
$recipeUnit = '';
|
||||
$recipeVal = 0;
|
||||
if (preg_match('/(\d+[.,]?\d*)\s*(g|gr|gramm|kg|ml|l|litri|cl|pz|pezz|conf)/i', $recipeQty, $qm)) {
|
||||
$recipeVal = (float)str_replace(',', '.', $qm[1]);
|
||||
$ru = strtolower($qm[2]);
|
||||
if (strpos($ru, 'g') === 0) $recipeUnit = 'g';
|
||||
elseif ($ru === 'kg') $recipeUnit = 'kg';
|
||||
elseif ($ru === 'ml') $recipeUnit = 'ml';
|
||||
elseif ($ru === 'cl') $recipeUnit = 'ml'; // cl→ml
|
||||
elseif ($ru === 'l' || strpos($ru, 'litr') === 0) $recipeUnit = 'l';
|
||||
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
|
||||
if ($recipeUnit === 'g' && $invUnit === 'kg') {
|
||||
$qtyNum = $recipeVal / 1000;
|
||||
} elseif ($recipeUnit === 'kg' && $invUnit === 'g') {
|
||||
$qtyNum = $recipeVal * 1000;
|
||||
// Volume conversions
|
||||
} 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 ($invUnit === 'pz' || $invUnit === 'conf') {
|
||||
$defQty = (float)($bestMatch['default_quantity'] ?? 0);
|
||||
if ($defQty > 0) {
|
||||
// Convert recipe grams/ml to pieces using default_quantity
|
||||
$qtyNum = $recipeVal / $defQty;
|
||||
$qtyNum = max(0.25, round($qtyNum * 4) / 4); // round to nearest quarter
|
||||
} else {
|
||||
$qtyNum = max(1, round($recipeVal / 100)); // fallback heuristic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check: qty_number should not exceed available
|
||||
if ($qtyNum > $invQty) {
|
||||
$qtyNum = $invQty; // cap to available
|
||||
}
|
||||
|
||||
// Sanity check: if qty_number is absurdly small relative to recipe
|
||||
// e.g. recipe says 100g but qty_number is 0.1 and unit is g → likely meant 100
|
||||
if ($recipeVal > 0 && $recipeUnit === $invUnit && $qtyNum < $recipeVal * 0.01) {
|
||||
$qtyNum = $recipeVal; // Gemini probably confused the units
|
||||
}
|
||||
|
||||
$ing['qty_number'] = round($qtyNum, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2536,6 +2536,14 @@ body {
|
||||
}
|
||||
|
||||
/* ===== LARGER QUANTITY IN INVENTORY ===== */
|
||||
.inv-qty-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inv-qty-prominent {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 800;
|
||||
@@ -2547,6 +2555,34 @@ body {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Package fraction indicators */
|
||||
.pkg-fraction {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pkg-fraction.pkg-full {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.inv-pkg-frac {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inv-status-total-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.inv-status-total-frac {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* ===== ALERT QUANTITY BADGES ===== */
|
||||
.alert-item-qty {
|
||||
font-size: 0.78rem;
|
||||
|
||||
+48
-1
@@ -709,6 +709,43 @@ function formatQuantity(qty, unit) {
|
||||
return `${n.toFixed(1)} ${label}`;
|
||||
}
|
||||
|
||||
// Show package fraction indicator (1/4, 1/2, 3/4, pieno) relative to original package size
|
||||
function formatPackageFraction(qty, defaultQty) {
|
||||
if (!defaultQty || defaultQty <= 0) return '';
|
||||
const n = parseFloat(qty);
|
||||
const d = parseFloat(defaultQty);
|
||||
if (isNaN(n) || isNaN(d) || d <= 0) return '';
|
||||
|
||||
const ratio = n / d;
|
||||
|
||||
// Multiple full packages
|
||||
const fullPkgs = Math.floor(ratio);
|
||||
const remainder = ratio - fullPkgs;
|
||||
|
||||
// Map remainder to closest readable fraction
|
||||
let fracLabel = '';
|
||||
if (remainder < 0.1) fracLabel = '';
|
||||
else if (remainder < 0.2) fracLabel = '⅛';
|
||||
else if (remainder < 0.38) fracLabel = '¼';
|
||||
else if (remainder < 0.62) fracLabel = '½';
|
||||
else if (remainder < 0.88) fracLabel = '¾';
|
||||
else { fracLabel = ''; } // close to full → count as full
|
||||
|
||||
// Near-full remainder counts as +1 full
|
||||
const effectiveFull = remainder >= 0.88 ? fullPkgs + 1 : fullPkgs;
|
||||
|
||||
if (effectiveFull >= 1 && fracLabel) {
|
||||
return `<span class="pkg-fraction" title="${ratio.toFixed(2)} confezioni">${effectiveFull} + ${fracLabel} conf</span>`;
|
||||
} else if (effectiveFull >= 2) {
|
||||
return `<span class="pkg-fraction" title="${ratio.toFixed(2)} confezioni">${effectiveFull} conf</span>`;
|
||||
} else if (effectiveFull === 1 && !fracLabel) {
|
||||
return `<span class="pkg-fraction pkg-full" title="1 confezione intera">● pieno</span>`;
|
||||
} else if (fracLabel) {
|
||||
return `<span class="pkg-fraction" title="${ratio.toFixed(2)} confezioni">${fracLabel} conf</span>`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// ===== INVENTORY =====
|
||||
async function loadInventory() {
|
||||
try {
|
||||
@@ -727,6 +764,7 @@ function renderInventoryItem(item) {
|
||||
const isExpired = days < 0;
|
||||
const isExpiring = !isExpired && days <= 7;
|
||||
const qtyDisplay = formatQuantity(item.quantity, item.unit);
|
||||
const pkgFrac = formatPackageFraction(item.quantity, item.default_quantity);
|
||||
|
||||
let expiryBadge = '';
|
||||
if (item.expiry_date) {
|
||||
@@ -752,7 +790,10 @@ function renderInventoryItem(item) {
|
||||
${expiryBadge}
|
||||
</div>
|
||||
</div>
|
||||
<div class="inv-qty-col">
|
||||
<span class="inv-qty-prominent">${qtyDisplay}</span>
|
||||
${pkgFrac ? `<span class="inv-pkg-frac">${pkgFrac}</span>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -1676,9 +1717,11 @@ function showProductAction() {
|
||||
statusBar.style.display = 'block';
|
||||
let totalQty = 0;
|
||||
const unit = inventoryItems[0].unit || 'pz';
|
||||
const defQty = inventoryItems[0].default_quantity || 0;
|
||||
const invHtml = inventoryItems.map(inv => {
|
||||
const locInfo = LOCATIONS[inv.location] || { icon: '📦', label: inv.location };
|
||||
const qtyStr = formatQuantity(inv.quantity, inv.unit);
|
||||
const pkgF = formatPackageFraction(inv.quantity, inv.default_quantity);
|
||||
totalQty += parseFloat(inv.quantity);
|
||||
let expiryStr = '';
|
||||
if (inv.expiry_date) {
|
||||
@@ -1688,15 +1731,19 @@ function showProductAction() {
|
||||
else if (d <= 7) expiryStr = ` · 🟡 Scade tra ${d}g`;
|
||||
else expiryStr = ` · 📅 ${formatDate(inv.expiry_date)}`;
|
||||
}
|
||||
return `<div class="inv-status-item"><span>${locInfo.icon} ${locInfo.label}${expiryStr}</span><span class="inv-status-qty">${qtyStr}</span></div>`;
|
||||
return `<div class="inv-status-item"><span>${locInfo.icon} ${locInfo.label}${expiryStr}</span><span class="inv-status-qty">${qtyStr}${pkgF ? ' ' + pkgF : ''}</span></div>`;
|
||||
}).join('');
|
||||
|
||||
const totalStr = formatQuantity(totalQty, unit);
|
||||
const totalFrac = formatPackageFraction(totalQty, defQty);
|
||||
|
||||
statusBar.innerHTML = `
|
||||
<div class="inv-status-header">
|
||||
<span class="inv-status-title">📦 Ce l'hai già!</span>
|
||||
<div class="inv-status-total-col">
|
||||
<span class="inv-status-total">${totalStr}</span>
|
||||
${totalFrac ? `<span class="inv-status-total-frac">${totalFrac}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="inv-status-items">${invHtml}</div>
|
||||
`;
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user