fix: pz×container multiplication, approx badge for null-total items
This commit is contained in:
+38
-4
@@ -6866,11 +6866,18 @@ function _calcEstimatedTotal(float $pricePerUnit, string $priceUnitLabel, float
|
|||||||
if ($defQty > 0 && !empty($pkgUnit) && strtolower($pkgUnit) === strtolower($unit)) {
|
if ($defQty > 0 && !empty($pkgUnit) && strtolower($pkgUnit) === strtolower($unit)) {
|
||||||
$pkgWeight = $defQty;
|
$pkgWeight = $defQty;
|
||||||
}
|
}
|
||||||
// 2) Extract weight from label: "confezione 250g", "vasetto 125ml", "pacco 500g"
|
// 2) Extract weight/volume from label: "confezione 250g", "vasetto 125ml", "pacco 500g",
|
||||||
|
// "pacco 1kg" (convert kg→g), "bottiglia 1.5L" (convert L→ml)
|
||||||
if ($pkgWeight <= 0) {
|
if ($pkgWeight <= 0) {
|
||||||
if (preg_match('/\b(\d+(?:[.,]\d+)?)\s*(g|ml)\b/i', $priceUnitLabel, $m)) {
|
if (preg_match('/\b(\d+(?:[.,]\d+)?)\s*(g|ml|kg|l|lt)\b/i', $priceUnitLabel, $m)) {
|
||||||
if (strtolower($m[2]) === strtolower($unit)) {
|
$rawVal = (float)str_replace(',', '.', $m[1]);
|
||||||
$pkgWeight = (float)str_replace(',', '.', $m[1]);
|
$rawUnit = strtolower($m[2]);
|
||||||
|
if ($rawUnit === strtolower($unit)) {
|
||||||
|
$pkgWeight = $rawVal;
|
||||||
|
} elseif ($rawUnit === 'kg' && strtolower($unit) === 'g') {
|
||||||
|
$pkgWeight = $rawVal * 1000.0;
|
||||||
|
} elseif (in_array($rawUnit, ['l', 'lt']) && strtolower($unit) === 'ml') {
|
||||||
|
$pkgWeight = $rawVal * 1000.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6886,6 +6893,33 @@ function _calcEstimatedTotal(float $pricePerUnit, string $priceUnitLabel, float
|
|||||||
return round($pricePerUnit, 2);
|
return round($pricePerUnit, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special case: unit='pz' (individual pieces) vs. container retail unit.
|
||||||
|
// If the AI priced per-container and the user requested individual pieces,
|
||||||
|
// buy ceil(qty / piecesPerContainer) containers — or just 1 if unknown.
|
||||||
|
if (strtolower($unit) === 'pz') {
|
||||||
|
static $containerKw = [
|
||||||
|
'confezione', 'pacco', 'pack', 'busta', 'sacchetto', 'vasetto',
|
||||||
|
'barattolo', 'rete', 'casco', 'mazzo', 'bottiglia', 'brick',
|
||||||
|
'lattina', 'latta', 'vaschetta', 'scatola', 'tray',
|
||||||
|
];
|
||||||
|
$isContainer = false;
|
||||||
|
foreach ($containerKw as $kw) {
|
||||||
|
if (str_contains($label, $kw)) { $isContainer = true; break; }
|
||||||
|
}
|
||||||
|
if ($isContainer) {
|
||||||
|
// Try to extract pieces-per-container from label (e.g. "confezione 6 uova" → 6).
|
||||||
|
// Ignore numbers followed by a weight/volume unit (e.g. "rete 1kg" → 0).
|
||||||
|
$pcsPerContainer = 0;
|
||||||
|
if (preg_match('/\b(\d+)\b(?!\s*(?:g|kg|ml|l|lt|cl|gr)\b)/i', $priceUnitLabel, $pm)) {
|
||||||
|
$pcsPerContainer = (int)$pm[1];
|
||||||
|
}
|
||||||
|
$containers = ($pcsPerContainer >= 2)
|
||||||
|
? (int) max(1, ceil($qty / $pcsPerContainer))
|
||||||
|
: 1;
|
||||||
|
return round($pricePerUnit * $containers, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$buyQty = max(1.0, $qty);
|
$buyQty = max(1.0, $qty);
|
||||||
return round($pricePerUnit * $buyQty, 2);
|
return round($pricePerUnit * $buyQty, 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1924,10 +1924,9 @@ body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
.price-col-qty {
|
.price-col-main.price-col-approx {
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 400;
|
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.price-col-unit {
|
.price-col-unit {
|
||||||
font-size: 0.62rem;
|
font-size: 0.62rem;
|
||||||
|
|||||||
+6
-12
@@ -8795,23 +8795,19 @@ function _buildPricePayload() {
|
|||||||
* @param {string} sym — currency symbol like "€"
|
* @param {string} sym — currency symbol like "€"
|
||||||
*/
|
*/
|
||||||
function _buildPriceBadgeHTML(entry, sym) {
|
function _buildPriceBadgeHTML(entry, sym) {
|
||||||
const isApprox = (entry.source_note || '').startsWith('~');
|
const hasTotal = entry.estimated_total != null;
|
||||||
|
const isApprox = !hasTotal || (entry.source_note || '').startsWith('~');
|
||||||
const mainLabel = (isApprox ? '~' : '')
|
const mainLabel = (isApprox ? '~' : '')
|
||||||
+ (entry.estimated_total != null
|
+ (hasTotal
|
||||||
? `${sym}${entry.estimated_total.toFixed(2)}`
|
? `${sym}${entry.estimated_total.toFixed(2)}`
|
||||||
: `${sym}${entry.price_per_unit.toFixed(2)}`);
|
: `${sym}${entry.price_per_unit.toFixed(2)}`);
|
||||||
const unitLabel = entry.unit_label || '';
|
const unitLabel = entry.unit_label || '';
|
||||||
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}${qtyLine ? `<span class=\"price-col-qty\">\u00a0·\u00a0${escapeHtml(qtyLine)}</span>` : ''}</div>`
|
const approxClass = isApprox ? ' price-col-approx' : '';
|
||||||
|
return `<div class="price-col-main${approxClass}" title="${escapeHtml(title)}">${mainLabel}</div>`
|
||||||
+ (unitLine ? `<div class="price-col-unit">${unitLine}</div>` : '');
|
+ (unitLine ? `<div class="price-col-unit">${unitLine}</div>` : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8940,7 +8936,7 @@ async function fetchAllPrices(forceRefresh = false) {
|
|||||||
shoppingItems.forEach((item, idx) => {
|
shoppingItems.forEach((item, idx) => {
|
||||||
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.price_per_unit != null) {
|
||||||
// Store with server-resolved qty/unit for correct cache validation
|
// Store with server-resolved qty/unit for correct cache validation
|
||||||
_cachedPrices[item.name] = {
|
_cachedPrices[item.name] = {
|
||||||
...entry,
|
...entry,
|
||||||
@@ -8949,8 +8945,6 @@ async function fetchAllPrices(forceRefresh = false) {
|
|||||||
};
|
};
|
||||||
if (badge) badge.innerHTML = _buildPriceBadgeHTML(entry, sym);
|
if (badge) badge.innerHTML = _buildPriceBadgeHTML(entry, sym);
|
||||||
} else {
|
} else {
|
||||||
// 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>`;
|
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=20260508a">
|
<link rel="stylesheet" href="assets/css/style.css?v=20260508b">
|
||||||
<!-- 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=20260508a"></script>
|
<script src="assets/js/app.js?v=20260508b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user