fix: recipe quantities for conf+weight; move modal remembers location
- PHP (all 3 recipe endpoints): conf products with weight/volume package_unit (e.g., 300g/conf) now keep qty_number in sub-units (grams/ml) instead of converting to fractional conf. Post-normalisation block converts any AI- returned fractional conf values to grams automatically. - JS submitRecipeUse: vacuum state now read from actual inventory item at the used location (_recipeUseContext.items), not from recipe ingredient data. - JS showRecipeMoveModal: now uses _prefMoveLocCache preference system — after 2 consistent choices the modal is skipped automatically. 'Stay' button records the choice. Added _recipeMoveCancelStay() helper. - JS confirmRecipeMove: records move choice via _recordMoveLocChoice(); accepts optional forcedVacuum param for preference-triggered auto-moves. Closes #108 (duplicate of #107 — data dir permissions)
This commit is contained in:
+63
-12
@@ -4519,6 +4519,7 @@ PROMPT;
|
||||
}
|
||||
|
||||
// Convert qty_number to inventory unit if mismatch detected
|
||||
$confAlreadyInSubUnit = false;
|
||||
if ($recipeUnit && $recipeUnit !== $invUnit) {
|
||||
// Weight conversions (both should be 'g' now, but handle legacy 'kg')
|
||||
if ($recipeUnit === 'g' && $invUnit === 'kg') {
|
||||
@@ -4530,22 +4531,31 @@ PROMPT;
|
||||
$qtyNum = $recipeVal / 1000;
|
||||
} elseif ($recipeUnit === 'ml' && $invUnit === 'ml') {
|
||||
$qtyNum = $recipeVal;
|
||||
// g/ml → pz/conf (approximate to nearest piece)
|
||||
} elseif ($invUnit === 'pz' || $invUnit === 'conf') {
|
||||
// g/ml → conf with weight/volume pkg_unit: keep in sub-units so JS modal works
|
||||
} elseif ($invUnit === 'conf') {
|
||||
$defQty = (float)($bestMatch['default_quantity'] ?? 0);
|
||||
$pkgUnitLC = strtolower($bestMatch['package_unit'] ?? '');
|
||||
if ($defQty > 0 && ($pkgUnitLC === 'g' || $pkgUnitLC === 'ml')
|
||||
&& ($recipeUnit === 'g' || $recipeUnit === 'ml')) {
|
||||
// Keep qty_number in sub-units; JS handles g↔conf conversion
|
||||
$qtyNum = $recipeVal;
|
||||
$ing['qty'] = round($qtyNum) . ' ' . $pkgUnitLC;
|
||||
$confAlreadyInSubUnit = true;
|
||||
} else {
|
||||
// conf without weight pkg_unit: fractional conf
|
||||
$qtyNum = $defQty > 0 ? max(0.25, round(($recipeVal / $defQty) * 4) / 4) : 1;
|
||||
}
|
||||
// g/ml → pz (approximate to nearest piece)
|
||||
} elseif ($invUnit === 'pz') {
|
||||
$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
|
||||
$qtyNum = max(0.25, round(($recipeVal / $defQty) * 4) / 4);
|
||||
} else {
|
||||
// No default_quantity: AI was told to use pieces but sent grams.
|
||||
// If the original qty_number looks like a piece count (≤ invQty and ≤ 100)
|
||||
// keep it; otherwise fall back to 1.
|
||||
$origQtyNum = (float)($ing['qty_number'] ?? 0);
|
||||
if ($origQtyNum >= 1 && $origQtyNum <= $invQty && $origQtyNum <= 100) {
|
||||
$qtyNum = $origQtyNum; // already a plausible piece count
|
||||
$qtyNum = $origQtyNum;
|
||||
} else {
|
||||
$qtyNum = 1; // safe minimum: 1 piece
|
||||
$qtyNum = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4557,6 +4567,17 @@ PROMPT;
|
||||
}
|
||||
}
|
||||
|
||||
// Conf+weight post-normalisation: if qty_number wasn't already set to
|
||||
// sub-units above, and it looks like a fractional conf value (≤ available
|
||||
// conf count), convert to grams so the JS modal shows correct grams.
|
||||
if (!$confAlreadyInSubUnit && $invUnit === 'conf' && $qtyNum > 0) {
|
||||
$defQty = (float)($bestMatch['default_quantity'] ?? 0);
|
||||
$pkgUnitLC = strtolower($bestMatch['package_unit'] ?? '');
|
||||
if ($defQty > 0 && ($pkgUnitLC === 'g' || $pkgUnitLC === 'ml') && $qtyNum <= $invQty) {
|
||||
$qtyNum = round($qtyNum * $defQty);
|
||||
$ing['qty'] = $qtyNum . ' ' . $pkgUnitLC;
|
||||
}
|
||||
}
|
||||
// Sanity check: qty_number should not exceed available
|
||||
if ($qtyNum > $invQty) {
|
||||
$qtyNum = $invQty; // cap to available
|
||||
@@ -4919,16 +4940,31 @@ function _enrichChatIngredients(array &$ingredients, array $items): void {
|
||||
elseif (strpos($ru, 'pz') === 0 || strpos($ru, 'pezz') === 0) $recipeUnit = 'pz';
|
||||
elseif (strpos($ru, 'conf') === 0) $recipeUnit = 'conf';
|
||||
}
|
||||
$confAlreadyInSubUnit = false;
|
||||
if ($recipeUnit && $recipeUnit !== $invUnit) {
|
||||
if ($recipeUnit === 'g' && $invUnit === 'g') $qtyNum = $recipeVal;
|
||||
elseif ($recipeUnit === 'g' && $invUnit === 'kg') $qtyNum = $recipeVal / 1000;
|
||||
elseif ($recipeUnit === 'ml' && $invUnit === 'ml') $qtyNum = $recipeVal;
|
||||
elseif ($recipeUnit === 'ml' && $invUnit === 'l') $qtyNum = $recipeVal / 1000;
|
||||
elseif ($invUnit === 'pz' || $invUnit === 'conf') {
|
||||
elseif ($invUnit === 'conf') {
|
||||
$defQty = (float)($bestMatch['default_quantity'] ?? 0);
|
||||
$pkgUnitLC = strtolower($bestMatch['package_unit'] ?? '');
|
||||
if ($defQty > 0 && ($pkgUnitLC === 'g' || $pkgUnitLC === 'ml') && ($recipeUnit === 'g' || $recipeUnit === 'ml')) {
|
||||
$qtyNum = $recipeVal; $ing['qty'] = round($qtyNum) . ' ' . $pkgUnitLC; $confAlreadyInSubUnit = true;
|
||||
} else { $qtyNum = $defQty > 0 ? max(0.25, round(($recipeVal / $defQty) * 4) / 4) : 1; }
|
||||
} elseif ($invUnit === 'pz') {
|
||||
$defQty = (float)($bestMatch['default_quantity'] ?? 0);
|
||||
$qtyNum = $defQty > 0 ? max(0.25, round(($recipeVal / $defQty) * 4) / 4) : max(1, round($recipeVal / 100));
|
||||
}
|
||||
}
|
||||
// Conf+weight: normalise fractional conf to sub-units
|
||||
if (!$confAlreadyInSubUnit && $invUnit === 'conf' && $qtyNum > 0) {
|
||||
$defQty = (float)($bestMatch['default_quantity'] ?? 0);
|
||||
$pkgUnitLC = strtolower($bestMatch['package_unit'] ?? '');
|
||||
if ($defQty > 0 && ($pkgUnitLC === 'g' || $pkgUnitLC === 'ml') && $qtyNum <= $invQty) {
|
||||
$qtyNum = round($qtyNum * $defQty); $ing['qty'] = $qtyNum . ' ' . $pkgUnitLC;
|
||||
}
|
||||
}
|
||||
if ($qtyNum > $invQty) $qtyNum = $invQty;
|
||||
if ($recipeVal > 0 && $recipeUnit === $invUnit && $qtyNum < $recipeVal * 0.01) $qtyNum = $recipeVal;
|
||||
$ing['qty_number'] = round($qtyNum, 3);
|
||||
@@ -5439,17 +5475,32 @@ PROMPT;
|
||||
elseif (strpos($ru, 'pz') === 0 || strpos($ru, 'pezz') === 0) $recipeUnit = 'pz';
|
||||
elseif (strpos($ru, 'conf') === 0) $recipeUnit = 'conf';
|
||||
}
|
||||
$confAlreadyInSubUnit = false;
|
||||
if ($recipeUnit && $recipeUnit !== $invUnit) {
|
||||
if ($recipeUnit === 'g' && $invUnit === 'kg') $qtyNum = $recipeVal / 1000;
|
||||
elseif ($recipeUnit === 'g' && $invUnit === 'g') $qtyNum = $recipeVal;
|
||||
elseif ($recipeUnit === 'ml' && $invUnit === 'l') $qtyNum = $recipeVal / 1000;
|
||||
elseif ($recipeUnit === 'ml' && $invUnit === 'ml') $qtyNum = $recipeVal;
|
||||
elseif ($invUnit === 'pz' || $invUnit === 'conf') {
|
||||
elseif ($invUnit === 'conf') {
|
||||
$defQty = (float)($bestMatch['default_quantity'] ?? 0);
|
||||
$pkgUnitLC = strtolower($bestMatch['package_unit'] ?? '');
|
||||
if ($defQty > 0 && ($pkgUnitLC === 'g' || $pkgUnitLC === 'ml') && ($recipeUnit === 'g' || $recipeUnit === 'ml')) {
|
||||
$qtyNum = $recipeVal; $ing['qty'] = round($qtyNum) . ' ' . $pkgUnitLC; $confAlreadyInSubUnit = true;
|
||||
} else { $qtyNum = $defQty > 0 ? max(0.25, round(($recipeVal / $defQty) * 4) / 4) : 1; }
|
||||
} elseif ($invUnit === 'pz') {
|
||||
$defQty = (float)($bestMatch['default_quantity'] ?? 0);
|
||||
if ($defQty > 0) { $qtyNum = $recipeVal / $defQty; $qtyNum = max(0.25, round($qtyNum * 4) / 4); }
|
||||
else $qtyNum = max(1, round($recipeVal / 100));
|
||||
}
|
||||
}
|
||||
// Conf+weight: normalise fractional conf to sub-units
|
||||
if (!$confAlreadyInSubUnit && $invUnit === 'conf' && $qtyNum > 0) {
|
||||
$defQty = (float)($bestMatch['default_quantity'] ?? 0);
|
||||
$pkgUnitLC = strtolower($bestMatch['package_unit'] ?? '');
|
||||
if ($defQty > 0 && ($pkgUnitLC === 'g' || $pkgUnitLC === 'ml') && $qtyNum <= $invQty) {
|
||||
$qtyNum = round($qtyNum * $defQty); $ing['qty'] = $qtyNum . ' ' . $pkgUnitLC;
|
||||
}
|
||||
}
|
||||
if ($qtyNum > $invQty) $qtyNum = $invQty;
|
||||
if ($recipeVal > 0 && $recipeUnit === $invUnit && $qtyNum < $recipeVal * 0.01) $qtyNum = $recipeVal;
|
||||
$ing['qty_number'] = round($qtyNum, 3);
|
||||
|
||||
+32
-8
@@ -12734,11 +12734,13 @@ async function submitRecipeUse(useAll) {
|
||||
setTimeout(() => showToast(t('recipes.finished_added_bring_toast'), 'info'), 1500);
|
||||
}
|
||||
|
||||
// Check low stock → Bring! prompt, then offer move
|
||||
// Check low stock → shopping prompt, then offer move
|
||||
const moveCallback = result.remaining > 0
|
||||
? () => setTimeout(() => {
|
||||
const ingData = _cachedRecipe?.recipe?.ingredients?.[_recipeUseContext?.idx];
|
||||
const wasVacuum = !!(ingData?.vacuum_sealed);
|
||||
// Get vacuum state from the actual inventory item at this location
|
||||
const cachedItems = _recipeUseContext?.items || [];
|
||||
const itemAtLoc = cachedItems.find(i => i.location === location);
|
||||
const wasVacuum = !!(itemAtLoc?.vacuum_sealed);
|
||||
showRecipeMoveModal(productId, location, result.remaining, result.opened_id, wasVacuum);
|
||||
}, 300)
|
||||
: null;
|
||||
@@ -12758,6 +12760,21 @@ async function submitRecipeUse(useAll) {
|
||||
}
|
||||
|
||||
function showRecipeMoveModal(productId, fromLoc, remaining, openedId, wasVacuum) {
|
||||
// Set context for recording the choice
|
||||
_pendingMoveCtx = { productId, fromLoc, openedId };
|
||||
|
||||
// If a preference exists, skip the modal entirely
|
||||
const prefMoveLoc = _getPreferredMoveLoc(productId, fromLoc);
|
||||
if (prefMoveLoc) {
|
||||
if (prefMoveLoc === fromLoc) {
|
||||
closeModal();
|
||||
} else {
|
||||
confirmRecipeMove(productId, fromLoc, prefMoveLoc, openedId, wasVacuum);
|
||||
}
|
||||
_pendingMoveCtx = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const otherLocs = Object.entries(LOCATIONS).filter(([k]) => k !== fromLoc);
|
||||
const locButtons = otherLocs.map(([k, v]) =>
|
||||
`<button type="button" class="loc-btn" onclick="clearMoveModalTimer();confirmRecipeMove(${productId}, '${fromLoc}', '${k}', ${openedId || 0})">${v.icon} ${v.label}</button>`
|
||||
@@ -12776,18 +12793,25 @@ function showRecipeMoveModal(productId, fromLoc, remaining, openedId, wasVacuum)
|
||||
<p style="margin-bottom:12px">${t('move.question_short').replace('{thing}', openedId ? t('move.thing_opened') : t('move.thing_rest'))}</p>
|
||||
<div class="location-selector">${locButtons}</div>
|
||||
${vacuumRow}
|
||||
<button type="button" id="btn-move-stay" class="btn btn-secondary full-width move-countdown-btn" style="margin-top:12px" onclick="clearMoveModalTimer();closeModal()">${t('move.stay_btn').replace('{location}', LOCATIONS[fromLoc]?.label || fromLoc)}</button>
|
||||
<button type="button" id="btn-move-stay" class="btn btn-secondary full-width move-countdown-btn" style="margin-top:12px" onclick="clearMoveModalTimer();_recipeMoveCancelStay(${productId}, '${fromLoc}', ${openedId || 0})">${t('move.stay_btn').replace('{location}', LOCATIONS[fromLoc]?.label || fromLoc)}</button>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('modal-overlay').style.display = 'flex';
|
||||
// Hide the native kiosk settings button while the modal is open (prevents touch bleed-through)
|
||||
try { if (typeof _kioskBridge !== 'undefined') _kioskBridge.setNativeSettingsVisible(false); } catch (_) {}
|
||||
startMoveModalCountdown('btn-move-stay', () => { closeModal(); });
|
||||
startMoveModalCountdown('btn-move-stay', () => { _recipeMoveCancelStay(productId, fromLoc, openedId || 0); });
|
||||
}
|
||||
|
||||
async function confirmRecipeMove(productId, fromLoc, toLoc, openedId) {
|
||||
function _recipeMoveCancelStay(productId, fromLoc, openedId) {
|
||||
_recordMoveLocChoice(productId, fromLoc, fromLoc);
|
||||
_pendingMoveCtx = null;
|
||||
closeModal();
|
||||
}
|
||||
|
||||
async function confirmRecipeMove(productId, fromLoc, toLoc, openedId, forcedVacuum) {
|
||||
clearMoveModalTimer();
|
||||
const newVacuum = document.getElementById('move-vacuum-check')?.checked ? 1 : 0;
|
||||
_recordMoveLocChoice(productId, fromLoc, toLoc);
|
||||
_pendingMoveCtx = null;
|
||||
const newVacuum = forcedVacuum !== undefined ? (forcedVacuum ? 1 : 0) : (document.getElementById('move-vacuum-check')?.checked ? 1 : 0);
|
||||
closeModal();
|
||||
try {
|
||||
if (openedId) {
|
||||
|
||||
Reference in New Issue
Block a user