Unify product cards and edit packaged amounts by pack
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m13s
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m13s
This commit is contained in:
@@ -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
|
||||
<div class="rounded-2xl border px-3 py-3" style="background:#393937; border-color:#444442;">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[16px] font-bold tabular-nums" style="color:#6ee7b7;">${esc(formatPackAwareAmount(qty, def.pantryUnit, packSize, packLabel))}</p>
|
||||
<p class="text-[16px] font-bold tabular-nums" style="color:#6ee7b7;">${esc(stockValueLabel)}</p>
|
||||
${stockSubLabel ? `<p class="text-[11px] mt-1" style="color:#9b978f;">${esc(stockSubLabel)}</p>` : ''}
|
||||
</div>
|
||||
<button type="button" class="ingredient-card-stock-toggle inline-flex items-center rounded-full px-2.5 py-1 text-[10px] font-semibold shrink-0" style="background:${state.stockEditorOpen ? '#23221e' : '#2f2f2d'}; color:${state.stockEditorOpen ? '#f2efe8' : '#d7d2c8'};">
|
||||
${esc(actionLabel)}
|
||||
@@ -337,13 +388,14 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
|
||||
<i class="fas fa-minus text-xs"></i>
|
||||
</button>
|
||||
<label class="flex-1 rounded-xl px-3 py-2 flex items-center justify-center gap-2" style="background:#2f2f2d;">
|
||||
<input type="number" min="0" step="${step}" value="${formatQty(draftQty)}" class="ingredient-card-stock-input w-20 bg-transparent text-center text-[14px] font-semibold tabular-nums outline-none appearance-none" style="color:#ddd6ca; background:transparent !important; border:none !important; box-shadow:none !important; -webkit-appearance:none; -moz-appearance:textfield;">
|
||||
<span class="text-[12px] font-medium shrink-0" style="color:#9b978f;">${esc(unit)}</span>
|
||||
<input type="number" min="0" step="${usesPackStep ? '1' : step}" value="${draftInputValue}" class="ingredient-card-stock-input w-20 bg-transparent text-center text-[14px] font-semibold tabular-nums outline-none appearance-none" style="color:#ddd6ca; background:transparent !important; border:none !important; box-shadow:none !important; -webkit-appearance:none; -moz-appearance:textfield;">
|
||||
<span class="text-[12px] font-medium shrink-0" style="color:#9b978f;">${esc(draftInputUnit)}</span>
|
||||
</label>
|
||||
<button type="button" class="ingredient-card-stock-step w-9 h-9 rounded-xl flex items-center justify-center shrink-0" style="background:#2f2f2d; color:#d7d2c8;" data-dir="1" aria-label="Zwiększ szkic zapasu">
|
||||
<i class="fas fa-plus text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
${usesPackStep ? `<p class="text-[10px] mt-2 text-right" style="color:#9b978f;">${esc(formatQtyWithUnit(draftQty, def.pantryUnit))}</p>` : ''}
|
||||
<div class="flex items-center justify-between gap-3 mt-3">
|
||||
<button type="button" class="ingredient-card-stock-clear text-[11px] font-semibold" style="color:#9b978f;">Wyzeruj</button>
|
||||
<button type="button" class="ingredient-card-stock-save inline-flex items-center rounded-full px-3 py-1.5 text-[11px] font-semibold" style="background:#ddd6ca; color:#2d2e2b;">Zapisz</button>
|
||||
@@ -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
|
||||
<div class="rounded-2xl border px-3 py-3" style="background:#393937; border-color:#444442;">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[16px] font-bold tabular-nums" style="color:${hasShoppingItem ? '#ddd6ca' : '#9b978f'};">${esc(hasShoppingItem ? formatPackAwareAmount(shoppingAmount, def.pantryUnit, packSize, packLabel) : 'Brak na liscie')}</p>
|
||||
<p class="text-[16px] font-bold tabular-nums" style="color:${hasShoppingItem ? '#ddd6ca' : '#9b978f'};">${esc(shopValueLabel)}</p>
|
||||
${shopSubLabel ? `<p class="text-[11px] mt-1" style="color:#9b978f;">${esc(shopSubLabel)}</p>` : ''}
|
||||
</div>
|
||||
<button type="button" class="ingredient-card-shop-toggle inline-flex items-center rounded-full px-2.5 py-1 text-[10px] font-semibold shrink-0" style="background:${state.shopEditorOpen ? '#23221e' : '#2f2f2d'}; color:${state.shopEditorOpen ? '#f2efe8' : '#d7d2c8'};">
|
||||
${esc(actionLabel)}
|
||||
@@ -491,13 +560,14 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
|
||||
<i class="fas fa-minus text-xs"></i>
|
||||
</button>
|
||||
<label class="flex-1 rounded-xl px-3 py-2 flex items-center justify-center gap-2" style="background:#2f2f2d;">
|
||||
<input type="number" min="0" step="${defaultAmount}" value="${formatQty(draftQty)}" class="ingredient-card-shop-input w-20 bg-transparent text-center text-[14px] font-semibold tabular-nums outline-none appearance-none" style="color:#ddd6ca; background:transparent !important; border:none !important; box-shadow:none !important; -webkit-appearance:none; -moz-appearance:textfield;">
|
||||
<span class="text-[12px] font-medium shrink-0" style="color:#9b978f;">${esc(unitLabel(def.pantryUnit))}</span>
|
||||
<input type="number" min="0" step="${usesPackStep ? '1' : defaultAmount}" value="${shopInputValue}" class="ingredient-card-shop-input w-20 bg-transparent text-center text-[14px] font-semibold tabular-nums outline-none appearance-none" style="color:#ddd6ca; background:transparent !important; border:none !important; box-shadow:none !important; -webkit-appearance:none; -moz-appearance:textfield;">
|
||||
<span class="text-[12px] font-medium shrink-0" style="color:#9b978f;">${esc(shopInputUnit)}</span>
|
||||
</label>
|
||||
<button type="button" class="ingredient-card-shop-step w-9 h-9 rounded-xl flex items-center justify-center shrink-0" style="background:#2f2f2d; color:#d7d2c8;" data-dir="1" aria-label="Zwiększ ilość na liście">
|
||||
<i class="fas fa-plus text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
${usesPackStep ? `<p class="text-[10px] mt-2 text-right" style="color:#9b978f;">${esc(formatQtyWithUnit(draftQty, def.pantryUnit))}</p>` : ''}
|
||||
<div class="flex items-center justify-between gap-3 mt-3">
|
||||
${hasShoppingItem
|
||||
? '<button type="button" class="ingredient-card-shop-remove text-[11px] font-semibold" style="color:#9b978f;">Usuń z listy</button>'
|
||||
@@ -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();
|
||||
|
||||
@@ -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, '>').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;
|
||||
|
||||
@@ -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 ── */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user