fix: shopping price calc — null for unconvertible /kg items, resolved qty in response/badge
This commit is contained in:
+13
-7
@@ -6733,6 +6733,8 @@ function getAllShoppingPrices(PDO $db): void {
|
|||||||
'estimated_total' => $est,
|
'estimated_total' => $est,
|
||||||
'estimated_total_label' => $est !== null ? _formatPrice($est, $currency) : null,
|
'estimated_total_label' => $est !== null ? _formatPrice($est, $currency) : null,
|
||||||
'from_cache' => true,
|
'from_cache' => true,
|
||||||
|
'_resolved_qty' => $qty,
|
||||||
|
'_resolved_unit' => $unit,
|
||||||
]);
|
]);
|
||||||
$total += $est ?? 0;
|
$total += $est ?? 0;
|
||||||
continue;
|
continue;
|
||||||
@@ -6775,6 +6777,8 @@ function getAllShoppingPrices(PDO $db): void {
|
|||||||
'estimated_total' => $est,
|
'estimated_total' => $est,
|
||||||
'estimated_total_label' => $est !== null ? _formatPrice($est, $currency) : null,
|
'estimated_total_label' => $est !== null ? _formatPrice($est, $currency) : null,
|
||||||
'from_cache' => false,
|
'from_cache' => false,
|
||||||
|
'_resolved_qty' => $qty,
|
||||||
|
'_resolved_unit' => $unit,
|
||||||
]);
|
]);
|
||||||
$total += $est ?? 0;
|
$total += $est ?? 0;
|
||||||
} else {
|
} else {
|
||||||
@@ -6818,34 +6822,36 @@ function _calcEstimatedTotal(float $pricePerUnit, string $priceUnitLabel, float
|
|||||||
// "pacco 500g" or "mazzo" fall through to the countable path below.
|
// "pacco 500g" or "mazzo" fall through to the countable path below.
|
||||||
if ($label === 'kg') {
|
if ($label === 'kg') {
|
||||||
$weightKg = 0.0;
|
$weightKg = 0.0;
|
||||||
if ($unit === 'conf' && $defQty > 0 && !empty($pkgUnit)) {
|
if (($unit === 'conf' || $unit === 'pz') && $defQty > 0 && !empty($pkgUnit)) {
|
||||||
|
// Each conf/pz weighs defQty pkgUnit (e.g. defQty=250, pkgUnit='g')
|
||||||
$sub = strtolower($pkgUnit);
|
$sub = strtolower($pkgUnit);
|
||||||
if ($sub === 'g') $weightKg = $qty * $defQty / 1000.0;
|
if ($sub === 'g') $weightKg = $qty * $defQty / 1000.0;
|
||||||
elseif ($sub === 'kg') $weightKg = $qty * $defQty;
|
elseif ($sub === 'kg') $weightKg = $qty * $defQty;
|
||||||
// unknown sub-unit: can't convert → return null
|
|
||||||
} elseif ($unit === 'g') {
|
} elseif ($unit === 'g') {
|
||||||
$weightKg = $qty / 1000.0;
|
$weightKg = $qty / 1000.0;
|
||||||
} elseif ($unit === 'kg') {
|
} elseif ($unit === 'kg') {
|
||||||
$weightKg = $qty;
|
$weightKg = $qty;
|
||||||
}
|
}
|
||||||
// pz / conf without defQty → assume 1 unit at price_per_unit (e.g. 1kg of fruit)
|
// Cannot convert unit to kg → return null instead of misleadingly using
|
||||||
if ($weightKg <= 0) return round($pricePerUnit, 2);
|
// price_per_unit as total (which would inflate estimates for items like
|
||||||
|
// "3 pz Salame" priced at €21.50/kg, returning €21.50 for just ~300g).
|
||||||
|
if ($weightKg <= 0) return null;
|
||||||
return round($pricePerUnit * $weightKg, 2);
|
return round($pricePerUnit * $weightKg, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Volume-based price (per liter) ────────────────────────────────────────
|
// ── Volume-based price (per liter) ────────────────────────────────────────
|
||||||
if (in_array($label, ['l', 'lt', 'litre', 'liter', 'litro'])) {
|
if (in_array($label, ['l', 'lt', 'litre', 'liter', 'litro'])) {
|
||||||
$volumeL = 0.0;
|
$volumeL = 0.0;
|
||||||
if ($unit === 'conf' && $defQty > 0 && !empty($pkgUnit)) {
|
if (($unit === 'conf' || $unit === 'pz') && $defQty > 0 && !empty($pkgUnit)) {
|
||||||
$sub = strtolower($pkgUnit);
|
$sub = strtolower($pkgUnit);
|
||||||
if ($sub === 'ml') $volumeL = $qty * $defQty / 1000.0;
|
if ($sub === 'ml') $volumeL = $qty * $defQty / 1000.0;
|
||||||
elseif ($sub === 'l') $volumeL = $qty * $defQty;
|
elseif ($sub === 'l') $volumeL = $qty * $defQty;
|
||||||
} elseif ($unit === 'ml') {
|
} elseif ($unit === 'ml') {
|
||||||
$volumeL = $qty / 1000.0;
|
$volumeL = $qty / 1000.0;
|
||||||
} elseif ($unit === 'l') {
|
} elseif ($unit === 'l') {
|
||||||
$volumeL = $qty;
|
$volumeL = $qty;
|
||||||
}
|
}
|
||||||
if ($volumeL <= 0) return round($pricePerUnit, 2);
|
if ($volumeL <= 0) return null;
|
||||||
return round($pricePerUnit * $volumeL, 2);
|
return round($pricePerUnit * $volumeL, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1924,6 +1924,11 @@ body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
.price-col-qty {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
.price-col-unit {
|
.price-col-unit {
|
||||||
font-size: 0.62rem;
|
font-size: 0.62rem;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
|
|||||||
+14
-6
@@ -8804,8 +8804,14 @@ function _buildPriceBadgeHTML(entry, sym) {
|
|||||||
const unitLine = unitLabel && entry.price_per_unit != null
|
const unitLine = unitLabel && entry.price_per_unit != null
|
||||||
? `${sym}${entry.price_per_unit.toFixed(2)}/${unitLabel}`
|
? `${sym}${entry.price_per_unit.toFixed(2)}/${unitLabel}`
|
||||||
: '';
|
: '';
|
||||||
|
// Show resolved qty so user can verify the computation
|
||||||
|
const resolvedQty = entry._resolved_qty ?? null;
|
||||||
|
const resolvedUnit = entry._resolved_unit ?? null;
|
||||||
|
const qtyLine = (resolvedQty != null && resolvedQty !== 1)
|
||||||
|
? `${resolvedQty}\u202f${resolvedUnit}`
|
||||||
|
: '';
|
||||||
const title = entry.source_note || '';
|
const title = entry.source_note || '';
|
||||||
return `<div class="price-col-main" title="${escapeHtml(title)}">${mainLabel}</div>`
|
return `<div class="price-col-main" title="${escapeHtml(title)}">${mainLabel}${qtyLine ? `<span class=\"price-col-qty\">\u00a0·\u00a0${escapeHtml(qtyLine)}</span>` : ''}</div>`
|
||||||
+ (unitLine ? `<div class="price-col-unit">${unitLine}</div>` : '');
|
+ (unitLine ? `<div class="price-col-unit">${unitLine}</div>` : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8935,15 +8941,17 @@ async function fetchAllPrices(forceRefresh = false) {
|
|||||||
const entry = prices[item.name];
|
const entry = prices[item.name];
|
||||||
const badge = document.getElementById(`price-badge-${idx}`);
|
const badge = document.getElementById(`price-badge-${idx}`);
|
||||||
if (entry && !entry.error && entry.estimated_total != null) {
|
if (entry && !entry.error && entry.estimated_total != null) {
|
||||||
// Store in client cache keyed by whatever qty the server used
|
// Store with server-resolved qty/unit for correct cache validation
|
||||||
_cachedPrices[item.name] = {
|
_cachedPrices[item.name] = {
|
||||||
...entry,
|
...entry,
|
||||||
_qty: entry._qty ?? entry.quantity ?? 1,
|
_qty: entry._resolved_qty ?? 1,
|
||||||
_unit: entry._unit ?? entry.unit ?? 'conf',
|
_unit: entry._resolved_unit ?? 'conf',
|
||||||
};
|
};
|
||||||
if (badge) badge.innerHTML = _buildPriceBadgeHTML(entry, sym);
|
if (badge) badge.innerHTML = _buildPriceBadgeHTML(entry, sym);
|
||||||
} else if (badge) {
|
} else {
|
||||||
badge.innerHTML = `<span class="price-col-error">–</span>`;
|
// Item has no estimate (unit conversion impossible or not found)
|
||||||
|
// Keep any old cache entry but don't update badge to stale value
|
||||||
|
if (badge) badge.innerHTML = `<span class="price-col-error">–</span>`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@
|
|||||||
<title>EverShelf</title>
|
<title>EverShelf</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png">
|
<link rel="icon" type="image/png" href="assets/img/logo/logo_icon.png">
|
||||||
<link rel="stylesheet" href="assets/css/style.css?v=20260507e">
|
<link rel="stylesheet" href="assets/css/style.css?v=20260508a">
|
||||||
<!-- QuaggaJS for barcode scanning -->
|
<!-- QuaggaJS for barcode scanning -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2@1.8.4/dist/quagga.min.js"></script>
|
||||||
<!-- @xenova/transformers: ES-module bootstrap that exposes a lazy category-classifier as window._categoryPipelinePromise -->
|
<!-- @xenova/transformers: ES-module bootstrap that exposes a lazy category-classifier as window._categoryPipelinePromise -->
|
||||||
@@ -1462,6 +1462,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="assets/js/app.js?v=20260507o"></script>
|
<script src="assets/js/app.js?v=20260508a"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user