diff --git a/js/ui/ingredientCard.js b/js/ui/ingredientCard.js index 8b6bde0..2254a55 100644 --- a/js/ui/ingredientCard.js +++ b/js/ui/ingredientCard.js @@ -107,6 +107,47 @@ function formatPackAwareAmount(amount, pantryUnit, packSize, packLabel) { return `${formatQty(qty)} ${unit}`; } +function normalizeQty(value) { + return Math.max(0, Math.round((Number(value) || 0) * 100) / 100); +} + +function formatPreciseQty(n) { + const rounded = Math.round((Number(n) || 0) * 1000) / 1000; + if (Number.isInteger(rounded)) return String(rounded); + return rounded.toFixed(3).replace(/0+$/, '').replace(/\.$/, ''); +} + +function formatPackCount(amount, packSize) { + if (!Number.isFinite(Number(packSize)) || Number(packSize) <= 0) return ''; + return `${formatPreciseQty((Number(amount) || 0) / Number(packSize))} opak.`; +} + +function getQtyStepMeta(def, product = null) { + const productPackSize = Number(product?.packSize); + if (Number.isFinite(productPackSize) && productPackSize > 0) { + return { + step: productPackSize, + usesPackStep: true, + stepLabel: product?.packLabel || formatQtyWithUnit(productPackSize, def.pantryUnit), + }; + } + + const ingredientPackSize = Number(def?.purchasePack?.amount); + if (Number.isFinite(ingredientPackSize) && ingredientPackSize > 0) { + return { + step: ingredientPackSize, + usesPackStep: true, + stepLabel: def.purchasePack?.label || formatQtyWithUnit(ingredientPackSize, def.pantryUnit), + }; + } + + return { + step: pantryQtyStep(def.id), + usesPackStep: false, + stepLabel: '', + }; +} + export function getIngredientCardHTML({ idBase, overlayClass = 'fixed inset-0 z-[70] hidden opacity-0 transition-opacity duration-200 flex items-center justify-center p-5', @@ -153,6 +194,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze ingredientId: null, productId: null, selectedProductId: null, + allowProductSelection: true, sourceNote: defaultSourceNote, onProductChange: null, onAfterChange: null, @@ -234,7 +276,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze } if (backBtn) { - backBtn.classList.toggle('hidden', !(hasProducts && state.productId)); + backBtn.classList.toggle('hidden', !(hasProducts && state.productId && state.allowProductSelection)); } } @@ -250,7 +292,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze const hasProducts = ingredientHasProducts(def.id); const unitScope = def.pantryUnit === 'ml' ? 'na 100 ml' : 'na 100 g'; const hint = product - ? 'dokładne dla produktu' + ? '' : hasProducts ? 'orientacyjnie dla składnika' : ''; @@ -311,12 +353,20 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze const qty = product ? (getPantryProducts(state.ingredientId, pantry).find((i) => i.productId === state.productId)?.qty || 0) : totalQty; - const step = product ? (product.packSize || pantryQtyStep(state.ingredientId)) : pantryQtyStep(state.ingredientId); + const { step, usesPackStep } = getQtyStepMeta(def, product); const packSize = product?.packSize || def.purchasePack?.amount || 0; const packLabel = product?.packLabel || def.purchasePack?.label || ''; const draftQty = state.stockEditorOpen - ? Math.max(0, Number(state.stockDraftQty ?? qty) || 0) + ? normalizeQty(state.stockDraftQty ?? qty) : qty; + const stockValueLabel = usesPackStep + ? formatPackCount(qty, step) + : formatPackAwareAmount(qty, def.pantryUnit, packSize, packLabel); + const stockSubLabel = usesPackStep ? formatQtyWithUnit(qty, def.pantryUnit) : ''; + const draftInputValue = usesPackStep + ? formatPreciseQty(draftQty / step) + : formatPreciseQty(draftQty); + const draftInputUnit = usesPackStep ? 'opak.' : unit; const actionLabel = state.stockEditorOpen ? 'Anuluj' : 'Zmień'; wrap.innerHTML = ` @@ -324,7 +374,8 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
-

${esc(formatPackAwareAmount(qty, def.pantryUnit, packSize, packLabel))}

+

${esc(stockValueLabel)}

+ ${stockSubLabel ? `

${esc(stockSubLabel)}

` : ''}
+ ${usesPackStep ? `

${esc(formatQtyWithUnit(draftQty, def.pantryUnit))}

` : ''}
@@ -366,14 +418,16 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze wrap.querySelectorAll('.ingredient-card-stock-step').forEach((btn) => { btn.addEventListener('click', () => { const dir = Number(btn.dataset.dir) || 1; - const next = Math.max(0, Math.round(((Number(state.stockDraftQty ?? qty) || 0) + step * dir) * 100) / 100); + const next = normalizeQty((Number(state.stockDraftQty ?? qty) || 0) + step * dir); state.stockDraftQty = next; render(); }); }); wrap.querySelector('.ingredient-card-stock-input')?.addEventListener('input', (event) => { - state.stockDraftQty = Math.max(0, Number(event.target.value) || 0); + state.stockDraftQty = usesPackStep + ? normalizeQty((Number(event.target.value) || 0) * step) + : normalizeQty(event.target.value); }); wrap.querySelector('.ingredient-card-stock-clear')?.addEventListener('click', () => { @@ -383,7 +437,9 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze wrap.querySelector('.ingredient-card-stock-save')?.addEventListener('click', () => { const input = wrap.querySelector('.ingredient-card-stock-input'); - const nextQty = Math.max(0, Math.round((Number(input?.value ?? state.stockDraftQty ?? qty) || 0) * 100) / 100); + const nextQty = usesPackStep + ? normalizeQty((Number(input?.value) || 0) * step) + : normalizeQty(input?.value ?? state.stockDraftQty ?? qty); if (state.productId) { setPantryProductQty(state.ingredientId, state.productId, nextQty); } else { @@ -421,7 +477,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze function renderProducts() { const wrap = el('products'); if (!wrap || !state.ingredientId) return; - if (!ingredientHasProducts(state.ingredientId)) { + if (!ingredientHasProducts(state.ingredientId) || !state.allowProductSelection) { wrap.innerHTML = ''; return; } @@ -459,18 +515,30 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze const def = INGREDIENTS[state.ingredientId]; if (!def) return; - const hasProducts = ingredientHasProducts(state.ingredientId); const product = state.productId ? PRODUCTS[state.productId] : null; + const { step, usesPackStep } = getQtyStepMeta(def, product); const packSize = product?.packSize || def.purchasePack?.amount; const packLabel = product?.packLabel || def.purchasePack?.label; const usesPacks = Boolean(packSize && packSize > 0); - const defaultAmount = usesPacks ? packSize : pantryQtyStep(state.ingredientId); + const defaultAmount = step; const shoppingItem = getCurrentShoppingItem(def); const hasShoppingItem = Boolean(shoppingItem); const shoppingAmount = shoppingItem?.amount || 0; const draftQty = state.shopEditorOpen - ? Math.max(0, Number(state.shopDraftQty ?? (shoppingAmount || defaultAmount)) || 0) + ? normalizeQty(state.shopDraftQty ?? (shoppingAmount || defaultAmount)) : shoppingAmount; + const shopValueLabel = hasShoppingItem + ? usesPackStep + ? formatPackCount(shoppingAmount, step) + : formatPackAwareAmount(shoppingAmount, def.pantryUnit, packSize, packLabel) + : 'Brak na liście'; + const shopSubLabel = hasShoppingItem && usesPackStep + ? formatQtyWithUnit(shoppingAmount, def.pantryUnit) + : ''; + const shopInputValue = usesPackStep + ? formatPreciseQty(draftQty / step) + : formatPreciseQty(draftQty); + const shopInputUnit = usesPackStep ? 'opak.' : unitLabel(def.pantryUnit); const actionLabel = state.shopEditorOpen ? 'Anuluj' : 'Zmień'; wrap.innerHTML = ` @@ -478,7 +546,8 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
-

${esc(hasShoppingItem ? formatPackAwareAmount(shoppingAmount, def.pantryUnit, packSize, packLabel) : 'Brak na liscie')}

+

${esc(shopValueLabel)}

+ ${shopSubLabel ? `

${esc(shopSubLabel)}

` : ''}
+ ${usesPackStep ? `

${esc(formatQtyWithUnit(draftQty, def.pantryUnit))}

` : ''}
${hasShoppingItem ? '' @@ -522,14 +592,16 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze wrap.querySelectorAll('.ingredient-card-shop-step').forEach((btn) => { btn.addEventListener('click', () => { const dir = Number(btn.dataset.dir) || 1; - const next = Math.max(0, Math.round(((Number(state.shopDraftQty ?? (shoppingAmount || defaultAmount)) || 0) + defaultAmount * dir) * 100) / 100); + const next = normalizeQty((Number(state.shopDraftQty ?? (shoppingAmount || defaultAmount)) || 0) + step * dir); state.shopDraftQty = next; render(); }); }); wrap.querySelector('.ingredient-card-shop-input')?.addEventListener('input', (event) => { - state.shopDraftQty = Math.max(0, Number(event.target.value) || 0); + state.shopDraftQty = usesPackStep + ? normalizeQty((Number(event.target.value) || 0) * step) + : normalizeQty(event.target.value); }); wrap.querySelector('.ingredient-card-shop-remove')?.addEventListener('click', () => { @@ -545,7 +617,9 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze wrap.querySelector('.ingredient-card-shop-save')?.addEventListener('click', () => { const input = wrap.querySelector('.ingredient-card-shop-input'); - const nextAmount = Math.max(0, Math.round((Number(input?.value ?? state.shopDraftQty ?? defaultAmount) || 0) * 100) / 100); + const nextAmount = usesPackStep + ? normalizeQty((Number(input?.value) || 0) * step) + : normalizeQty(input?.value ?? state.shopDraftQty ?? defaultAmount); let toastText = null; if (shoppingItem) { updateKitchenItemAmount(KITCHEN_LIST_ID, shoppingItem.id, nextAmount); @@ -593,6 +667,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze ingredientId, productId = null, selectedProductId = productId, + allowProductSelection = true, sourceNote = defaultSourceNote, onProductChange = null, onAfterChange = null, @@ -605,6 +680,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze state.ingredientId = ingredientId; state.productId = productId && PRODUCTS[productId] ? productId : null; state.selectedProductId = selectedProductId && PRODUCTS[selectedProductId] ? selectedProductId : state.productId; + state.allowProductSelection = Boolean(allowProductSelection); state.sourceNote = sourceNote; state.onProductChange = onProductChange; state.onAfterChange = onAfterChange; @@ -634,6 +710,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze state.ingredientId = null; state.productId = null; state.selectedProductId = null; + state.allowProductSelection = true; state.onProductChange = null; state.onAfterChange = null; state.sourceNote = defaultSourceNote; @@ -645,6 +722,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze bound = true; el('close')?.addEventListener('click', close); el('back')?.addEventListener('click', () => { + if (!state.allowProductSelection) return; state.productId = null; resetInlineEditors(); render(); diff --git a/js/ui/mealPlanEditor.js b/js/ui/mealPlanEditor.js index 5381cbb..b1d90d6 100644 --- a/js/ui/mealPlanEditor.js +++ b/js/ui/mealPlanEditor.js @@ -26,7 +26,7 @@ import { renderCalendarGrid, syncCalendarTodayButton, } from './mealCalendar.js?v=1'; -import { createIngredientCardController, getIngredientCardHTML } from './ingredientCard.js?v=20260410-106'; +import { createIngredientCardController, getIngredientCardHTML } from './ingredientCard.js?v=20260410-107'; function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); @@ -674,6 +674,7 @@ export function setupMealPlanEditor() { ingredientId: eid, productId: cardBtn.dataset.pid || null, selectedProductId: cardBtn.dataset.pid || null, + allowProductSelection: !cardBtn.dataset.pid, sourceNote: 'Z planera', onProductChange: (nextProductId) => { if (!nextProductId) return; diff --git a/js/views/Pantry.js b/js/views/Pantry.js index d30d638..ed03dd7 100644 --- a/js/views/Pantry.js +++ b/js/views/Pantry.js @@ -9,7 +9,7 @@ import { loadPantry, getPantryTotal, } from '../services/pantryShopping.js?v=2'; -import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260410-106'; +import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260410-107'; /* ── helpers ── */