Redesign ingredient card
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:
@@ -11,7 +11,7 @@
|
|||||||
<meta http-equiv="Pragma" content="no-cache">
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
<meta http-equiv="Expires" content="0">
|
<meta http-equiv="Expires" content="0">
|
||||||
<title>Recipe App - Modular</title>
|
<title>Recipe App - Modular</title>
|
||||||
<link rel="manifest" href="./manifest.webmanifest?v=20260410-99">
|
<link rel="manifest" href="./manifest.webmanifest?v=20260410-106">
|
||||||
<link rel="icon" type="image/png" sizes="192x192" href="./icons/icon-192.png">
|
<link rel="icon" type="image/png" sizes="192x192" href="./icons/icon-192.png">
|
||||||
<link rel="apple-touch-icon" href="./icons/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="./icons/apple-touch-icon.png">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
@@ -600,7 +600,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const APP_ASSET_VERSION = '20260410-99';
|
const APP_ASSET_VERSION = '20260410-106';
|
||||||
const APP_VERSION_STORAGE_KEY = 'recipe-app-asset-version';
|
const APP_VERSION_STORAGE_KEY = 'recipe-app-asset-version';
|
||||||
const APP_VERSION_QUERY_KEY = 'appv';
|
const APP_VERSION_QUERY_KEY = 'appv';
|
||||||
|
|
||||||
@@ -634,7 +634,7 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
const appVersion = window.__APP_ASSET_VERSION__ || '20260410-99';
|
const appVersion = window.__APP_ASSET_VERSION__ || '20260410-106';
|
||||||
const recoveryKey = `recipe-app-recovery-${appVersion}`;
|
const recoveryKey = `recipe-app-recovery-${appVersion}`;
|
||||||
|
|
||||||
function renderBootstrapError(message) {
|
function renderBootstrapError(message) {
|
||||||
|
|||||||
@@ -8,11 +8,15 @@ import {
|
|||||||
} from '../data/catalog.js?v=8';
|
} from '../data/catalog.js?v=8';
|
||||||
import {
|
import {
|
||||||
addOrMergeShoppingLines,
|
addOrMergeShoppingLines,
|
||||||
|
KITCHEN_LIST_ID,
|
||||||
loadPantry,
|
loadPantry,
|
||||||
|
loadShoppingState,
|
||||||
|
removeItemFromList,
|
||||||
setPantryQty,
|
setPantryQty,
|
||||||
setPantryProductQty,
|
setPantryProductQty,
|
||||||
getPantryTotal,
|
getPantryTotal,
|
||||||
getPantryProducts,
|
getPantryProducts,
|
||||||
|
updateKitchenItemAmount,
|
||||||
} from '../services/pantryShopping.js?v=2';
|
} from '../services/pantryShopping.js?v=2';
|
||||||
import { showAppToast } from './toast.js';
|
import { showAppToast } from './toast.js';
|
||||||
|
|
||||||
@@ -91,6 +95,18 @@ function sortProductsByStock(products, pantryItems) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatPackAwareAmount(amount, pantryUnit, packSize, packLabel) {
|
||||||
|
const qty = Number(amount) || 0;
|
||||||
|
const unit = unitLabel(pantryUnit);
|
||||||
|
if (packSize && packSize > 0) {
|
||||||
|
const packs = qty / packSize;
|
||||||
|
if (qty > 0 && Number.isFinite(packs) && Math.abs(packs - Math.round(packs)) < 0.001) {
|
||||||
|
return `${formatQty(Math.round(packs))} x ${packLabel || `${formatQty(packSize)} ${unit}`}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${formatQty(qty)} ${unit}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function getIngredientCardHTML({
|
export function getIngredientCardHTML({
|
||||||
idBase,
|
idBase,
|
||||||
overlayClass = 'fixed inset-0 z-[70] hidden opacity-0 transition-opacity duration-200 flex items-center justify-center p-5',
|
overlayClass = 'fixed inset-0 z-[70] hidden opacity-0 transition-opacity duration-200 flex items-center justify-center p-5',
|
||||||
@@ -140,12 +156,36 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
|
|||||||
sourceNote: defaultSourceNote,
|
sourceNote: defaultSourceNote,
|
||||||
onProductChange: null,
|
onProductChange: null,
|
||||||
onAfterChange: null,
|
onAfterChange: null,
|
||||||
|
stockEditorOpen: false,
|
||||||
|
stockDraftQty: null,
|
||||||
|
shopEditorOpen: false,
|
||||||
|
shopDraftQty: null,
|
||||||
closeTimer: null,
|
closeTimer: null,
|
||||||
};
|
};
|
||||||
let bound = false;
|
let bound = false;
|
||||||
|
|
||||||
const el = (suffix = '') => document.getElementById(suffix ? `${idBase}-${suffix}` : idBase);
|
const el = (suffix = '') => document.getElementById(suffix ? `${idBase}-${suffix}` : idBase);
|
||||||
|
|
||||||
|
function resetInlineEditors() {
|
||||||
|
state.stockEditorOpen = false;
|
||||||
|
state.stockDraftQty = null;
|
||||||
|
state.shopEditorOpen = false;
|
||||||
|
state.shopDraftQty = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentShoppingItem(def) {
|
||||||
|
const shopping = loadShoppingState();
|
||||||
|
const kitchen = shopping.lists.find((list) => list.id === KITCHEN_LIST_ID && list.type === 'kitchen');
|
||||||
|
if (!kitchen || kitchen.type !== 'kitchen') return null;
|
||||||
|
const unit = unitLabel(def.pantryUnit);
|
||||||
|
return kitchen.items.find((item) => {
|
||||||
|
if (item.checked) return false;
|
||||||
|
return item.ingredientId === state.ingredientId
|
||||||
|
&& item.unit === unit
|
||||||
|
&& (item.productId || '') === (state.productId || '');
|
||||||
|
}) || null;
|
||||||
|
}
|
||||||
|
|
||||||
function renderHeader(def, product, pantry) {
|
function renderHeader(def, product, pantry) {
|
||||||
const hasProducts = ingredientHasProducts(def.id);
|
const hasProducts = ingredientHasProducts(def.id);
|
||||||
const icon = CATEGORY_ICONS[def.category] || 'fa-jar';
|
const icon = CATEGORY_ICONS[def.category] || 'fa-jar';
|
||||||
@@ -213,11 +253,12 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
|
|||||||
? 'dokładne dla produktu'
|
? 'dokładne dla produktu'
|
||||||
: hasProducts
|
: hasProducts
|
||||||
? 'orientacyjnie dla składnika'
|
? 'orientacyjnie dla składnika'
|
||||||
: 'bazowe wartości';
|
: '';
|
||||||
|
const nutritionMeta = hint ? `${unitScope} • ${hint}` : unitScope;
|
||||||
|
|
||||||
wrap.innerHTML = `
|
wrap.innerHTML = `
|
||||||
<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Wartości odżywcze</p>
|
<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Wartości odżywcze</p>
|
||||||
<p class="text-[10px] mb-1.5" style="color:#9b978f;">${esc(unitScope)} • ${esc(hint)}</p>
|
<p class="text-[10px] mb-1.5" style="color:#9b978f;">${esc(nutritionMeta)}</p>
|
||||||
<div class="grid grid-cols-4 gap-1.5">
|
<div class="grid grid-cols-4 gap-1.5">
|
||||||
<div class="rounded-xl px-2 py-1.5 text-center" style="background:#393937;">
|
<div class="rounded-xl px-2 py-1.5 text-center" style="background:#393937;">
|
||||||
<p class="text-[15px] font-bold tabular-nums leading-tight" style="color:#ddd6ca;">${nutrition.kcal}</p>
|
<p class="text-[15px] font-bold tabular-nums leading-tight" style="color:#ddd6ca;">${nutrition.kcal}</p>
|
||||||
@@ -252,18 +293,17 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
|
|||||||
|
|
||||||
if (hasProducts && !product) {
|
if (hasProducts && !product) {
|
||||||
const stockedCount = getPantryProducts(state.ingredientId, pantry).filter((i) => i.qty > 0).length;
|
const stockedCount = getPantryProducts(state.ingredientId, pantry).filter((i) => i.qty > 0).length;
|
||||||
const summaryNutrition = nutritionForQty(def, totalQty);
|
|
||||||
wrap.innerHTML = `
|
wrap.innerHTML = `
|
||||||
<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Zapas</p>
|
<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Zapas</p>
|
||||||
<div class="rounded-xl px-3 py-2.5" style="background:#393937;">
|
<div class="rounded-2xl border px-3 py-3" style="background:#393937; border-color:#444442;">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div class="min-w-0 flex-1">
|
||||||
<p class="text-[17px] font-bold tabular-nums" style="color:#6ee7b7;">${esc(formatQty(totalQty))} ${esc(unit)}</p>
|
<p class="text-[10px] font-semibold uppercase tracking-wide" style="color:#9b978f;">Stan łączny</p>
|
||||||
<p class="text-[11px] mt-0.5" style="color:#9b978f;">${stockedCount} z ${getProductsForIngredient(state.ingredientId).length} produktów ma stan</p>
|
<p class="text-[16px] font-bold tabular-nums mt-1" style="color:#6ee7b7;">${esc(formatQty(totalQty))} ${esc(unit)}</p>
|
||||||
|
<p class="text-[11px] mt-1 leading-snug" style="color:#9b978f;">${stockedCount} z ${getProductsForIngredient(state.ingredientId).length} produktów ma zapas</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-[10px] text-right max-w-[92px]" style="color:#9b978f;">Wybierz produkt niżej, aby zmienić stan</span>
|
<span class="inline-flex items-center rounded-full px-2 py-1 text-[10px] font-semibold shrink-0" style="background:#2f2f2d; color:#d7d2c8;">Wybierz produkt</span>
|
||||||
</div>
|
</div>
|
||||||
${summaryNutrition ? `<p class="text-[10px] mt-2 tabular-nums" style="color:#9b978f;">${esc(macroLine(summaryNutrition))}</p>` : ''}
|
|
||||||
</div>`;
|
</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -272,41 +312,87 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
|
|||||||
? (getPantryProducts(state.ingredientId, pantry).find((i) => i.productId === state.productId)?.qty || 0)
|
? (getPantryProducts(state.ingredientId, pantry).find((i) => i.productId === state.productId)?.qty || 0)
|
||||||
: totalQty;
|
: totalQty;
|
||||||
const step = product ? (product.packSize || pantryQtyStep(state.ingredientId)) : pantryQtyStep(state.ingredientId);
|
const step = product ? (product.packSize || pantryQtyStep(state.ingredientId)) : pantryQtyStep(state.ingredientId);
|
||||||
const stockNutrition = nutritionForQty(def, qty, product?.nutritionPer100g || def.nutritionPer100g);
|
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)
|
||||||
|
: qty;
|
||||||
|
const actionLabel = state.stockEditorOpen ? 'Anuluj' : 'Zmień';
|
||||||
|
|
||||||
wrap.innerHTML = `
|
wrap.innerHTML = `
|
||||||
<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Zapas</p>
|
<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Zapas</p>
|
||||||
<div class="flex items-center justify-center gap-3 rounded-xl px-3 py-2" style="background:#393937;">
|
<div class="rounded-2xl border px-3 py-3" style="background:#393937; border-color:#444442;">
|
||||||
<button type="button" class="ingredient-card-stock-btn w-9 h-9 rounded-xl flex items-center justify-center active:scale-95" style="background:#2f2f2d; color:#d7d2c8;" data-pid="${esc(state.productId || '_generic')}" data-step="${step}" data-dir="-1" aria-label="Zmniejsz stan">
|
<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>
|
||||||
|
</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)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${state.stockEditorOpen ? `
|
||||||
|
<div class="mt-3 pt-3 border-t" style="border-color:#444442;">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<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="Zmniejsz szkic zapasu">
|
||||||
<i class="fas fa-minus text-xs"></i>
|
<i class="fas fa-minus text-xs"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex-1 text-center">
|
<label class="flex-1 rounded-xl px-3 py-2 flex items-center justify-center gap-2" style="background:#2f2f2d;">
|
||||||
<p class="text-[17px] font-bold tabular-nums" style="color:#6ee7b7;">${esc(formatQty(qty))} ${esc(unit)}</p>
|
<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;">
|
||||||
<p class="text-[10px] mt-0.5" style="color:#9b978f;">Krok: ${esc(formatQty(step))} ${esc(unit)}</p>
|
<span class="text-[12px] font-medium shrink-0" style="color:#9b978f;">${esc(unit)}</span>
|
||||||
</div>
|
</label>
|
||||||
<button type="button" class="ingredient-card-stock-btn w-9 h-9 rounded-xl flex items-center justify-center active:scale-95" style="background:#2f2f2d; color:#d7d2c8;" data-pid="${esc(state.productId || '_generic')}" data-step="${step}" data-dir="1" aria-label="Zwiększ stan">
|
<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>
|
<i class="fas fa-plus text-xs"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
${stockNutrition ? `<p class="text-[10px] mt-1.5 tabular-nums" style="color:#9b978f;">${esc(macroLine(stockNutrition))} na stanie</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>
|
||||||
|
</div>
|
||||||
|
</div>` : ''}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
wrap.querySelectorAll('.ingredient-card-stock-btn').forEach((btn) => {
|
wrap.querySelector('.ingredient-card-stock-toggle')?.addEventListener('click', () => {
|
||||||
btn.addEventListener('click', () => {
|
if (state.stockEditorOpen) {
|
||||||
const pid = btn.dataset.pid;
|
state.stockEditorOpen = false;
|
||||||
const stepVal = Number(btn.dataset.step) || 1;
|
state.stockDraftQty = null;
|
||||||
const dir = Number(btn.dataset.dir) || 1;
|
|
||||||
const pantryState = loadPantry();
|
|
||||||
if (pid === '_generic') {
|
|
||||||
const current = getPantryTotal(state.ingredientId, pantryState);
|
|
||||||
setPantryQty(state.ingredientId, Math.max(0, current + stepVal * dir));
|
|
||||||
} else {
|
} else {
|
||||||
const items = getPantryProducts(state.ingredientId, pantryState);
|
state.stockEditorOpen = true;
|
||||||
const current = items.find((i) => i.productId === pid)?.qty || 0;
|
state.shopEditorOpen = false;
|
||||||
setPantryProductQty(state.ingredientId, pid, Math.max(0, current + stepVal * dir));
|
state.stockDraftQty = qty;
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
state.onAfterChange?.();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
state.stockDraftQty = next;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
wrap.querySelector('.ingredient-card-stock-input')?.addEventListener('input', (event) => {
|
||||||
|
state.stockDraftQty = Math.max(0, Number(event.target.value) || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
wrap.querySelector('.ingredient-card-stock-clear')?.addEventListener('click', () => {
|
||||||
|
state.stockDraftQty = 0;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (state.productId) {
|
||||||
|
setPantryProductQty(state.ingredientId, state.productId, nextQty);
|
||||||
|
} else {
|
||||||
|
setPantryQty(state.ingredientId, nextQty);
|
||||||
|
}
|
||||||
|
state.stockEditorOpen = false;
|
||||||
|
state.stockDraftQty = null;
|
||||||
|
render();
|
||||||
|
state.onAfterChange?.();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,6 +445,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
|
|||||||
const nextProductId = btn.dataset.productId || null;
|
const nextProductId = btn.dataset.productId || null;
|
||||||
state.productId = nextProductId;
|
state.productId = nextProductId;
|
||||||
state.selectedProductId = nextProductId;
|
state.selectedProductId = nextProductId;
|
||||||
|
resetInlineEditors();
|
||||||
if (nextProductId) state.onProductChange?.(nextProductId);
|
if (nextProductId) state.onProductChange?.(nextProductId);
|
||||||
render();
|
render();
|
||||||
state.onAfterChange?.();
|
state.onAfterChange?.();
|
||||||
@@ -377,28 +464,99 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
|
|||||||
const packSize = product?.packSize || def.purchasePack?.amount;
|
const packSize = product?.packSize || def.purchasePack?.amount;
|
||||||
const packLabel = product?.packLabel || def.purchasePack?.label;
|
const packLabel = product?.packLabel || def.purchasePack?.label;
|
||||||
const usesPacks = Boolean(packSize && packSize > 0);
|
const usesPacks = Boolean(packSize && packSize > 0);
|
||||||
const btnLabel = usesPacks
|
const defaultAmount = usesPacks ? packSize : pantryQtyStep(state.ingredientId);
|
||||||
? `Dodaj na listę (${packLabel || `${formatQty(packSize)} ${unitLabel(def.pantryUnit)}`})`
|
const shoppingItem = getCurrentShoppingItem(def);
|
||||||
: 'Dodaj na listę';
|
const hasShoppingItem = Boolean(shoppingItem);
|
||||||
const helperText = hasProducts && !product
|
const shoppingAmount = shoppingItem?.amount || 0;
|
||||||
? 'Doda składnik bez wskazanej marki. Jeśli chcesz konkretny produkt, wybierz go wyżej.'
|
const draftQty = state.shopEditorOpen
|
||||||
: product
|
? Math.max(0, Number(state.shopDraftQty ?? (shoppingAmount || defaultAmount)) || 0)
|
||||||
? 'Pozycja trafi na listę zakupów z dokładnym produktem.'
|
: shoppingAmount;
|
||||||
: `Szybki skrót do listy zakupów ${defaultSourceNote === 'Ze spiżarni' ? 'ze spiżarni' : 'z planera'}.`;
|
const actionLabel = state.shopEditorOpen ? 'Anuluj' : 'Zmień';
|
||||||
|
|
||||||
wrap.innerHTML = `
|
wrap.innerHTML = `
|
||||||
<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Lista zakupów</p>
|
<p class="text-[9px] font-semibold uppercase tracking-wide mb-1.5" style="color:#9b978f;">Lista zakupów</p>
|
||||||
<p class="text-[10px] mb-1.5" style="color:#9b978f;">${esc(helperText)}</p>
|
<div class="rounded-2xl border px-3 py-3" style="background:#393937; border-color:#444442;">
|
||||||
<button type="button" class="ingredient-card-add-list w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-[13px] font-semibold transition-colors active:scale-[0.98]" style="background:#ddd6ca; color:#2d2e2b;">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<i class="fas fa-cart-plus text-[11px]"></i>${esc(btnLabel)}
|
<div class="min-w-0 flex-1">
|
||||||
</button>`;
|
<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>
|
||||||
|
</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)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
${state.shopEditorOpen ? `
|
||||||
|
<div class="mt-3 pt-3 border-t" style="border-color:#444442;">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<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="Zmniejsz ilość na liście">
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
<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>'
|
||||||
|
: '<span></span>'}
|
||||||
|
<button type="button" class="ingredient-card-shop-save inline-flex items-center rounded-full px-3 py-1.5 text-[11px] font-semibold" style="background:#ddd6ca; color:#2d2e2b;">Zapisz</button>
|
||||||
|
</div>
|
||||||
|
</div>` : ''}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
wrap.querySelector('.ingredient-card-add-list')?.addEventListener('click', () => {
|
wrap.querySelector('.ingredient-card-shop-toggle')?.addEventListener('click', () => {
|
||||||
const amount = usesPacks ? packSize : pantryQtyStep(state.ingredientId);
|
if (state.shopEditorOpen) {
|
||||||
|
state.shopEditorOpen = false;
|
||||||
|
state.shopDraftQty = null;
|
||||||
|
} else {
|
||||||
|
state.shopEditorOpen = true;
|
||||||
|
state.stockEditorOpen = false;
|
||||||
|
state.shopDraftQty = shoppingAmount || defaultAmount;
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
state.shopDraftQty = next;
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
wrap.querySelector('.ingredient-card-shop-input')?.addEventListener('input', (event) => {
|
||||||
|
state.shopDraftQty = Math.max(0, Number(event.target.value) || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
wrap.querySelector('.ingredient-card-shop-remove')?.addEventListener('click', () => {
|
||||||
|
if (!shoppingItem) return;
|
||||||
|
removeItemFromList(KITCHEN_LIST_ID, shoppingItem.id);
|
||||||
|
state.shopEditorOpen = false;
|
||||||
|
state.shopDraftQty = null;
|
||||||
|
render();
|
||||||
|
state.onAfterChange?.();
|
||||||
|
window.refreshShopping?.();
|
||||||
|
showAppToast(`Usunieto ${product?.name || def.name} z listy.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
let toastText = null;
|
||||||
|
if (shoppingItem) {
|
||||||
|
updateKitchenItemAmount(KITCHEN_LIST_ID, shoppingItem.id, nextAmount);
|
||||||
|
toastText = nextAmount > 0
|
||||||
|
? `Zaktualizowano ${product?.name || def.name}.`
|
||||||
|
: `Usunieto ${product?.name || def.name} z listy.`;
|
||||||
|
} else if (nextAmount > 0) {
|
||||||
const note = usesPacks ? (packLabel || `${formatQty(packSize)} ${unitLabel(def.pantryUnit)}`) : undefined;
|
const note = usesPacks ? (packLabel || `${formatQty(packSize)} ${unitLabel(def.pantryUnit)}`) : undefined;
|
||||||
const line = {
|
const line = {
|
||||||
ingredientId: state.ingredientId,
|
ingredientId: state.ingredientId,
|
||||||
amount,
|
amount: nextAmount,
|
||||||
unit: unitLabel(def.pantryUnit),
|
unit: unitLabel(def.pantryUnit),
|
||||||
name: product?.name || def.name,
|
name: product?.name || def.name,
|
||||||
category: def.category,
|
category: def.category,
|
||||||
@@ -406,8 +564,14 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
|
|||||||
};
|
};
|
||||||
if (state.productId) line.productId = state.productId;
|
if (state.productId) line.productId = state.productId;
|
||||||
addOrMergeShoppingLines([line]);
|
addOrMergeShoppingLines([line]);
|
||||||
showAppToast(`Dodano ${product?.name || def.name} na listę.`);
|
toastText = `Dodano ${product?.name || def.name}.`;
|
||||||
|
}
|
||||||
|
state.shopEditorOpen = false;
|
||||||
|
state.shopDraftQty = null;
|
||||||
|
render();
|
||||||
|
state.onAfterChange?.();
|
||||||
window.refreshShopping?.();
|
window.refreshShopping?.();
|
||||||
|
if (toastText) showAppToast(toastText);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,6 +608,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
|
|||||||
state.sourceNote = sourceNote;
|
state.sourceNote = sourceNote;
|
||||||
state.onProductChange = onProductChange;
|
state.onProductChange = onProductChange;
|
||||||
state.onAfterChange = onAfterChange;
|
state.onAfterChange = onAfterChange;
|
||||||
|
resetInlineEditors();
|
||||||
render();
|
render();
|
||||||
|
|
||||||
clearTimeout(state.closeTimer);
|
clearTimeout(state.closeTimer);
|
||||||
@@ -472,6 +637,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
|
|||||||
state.onProductChange = null;
|
state.onProductChange = null;
|
||||||
state.onAfterChange = null;
|
state.onAfterChange = null;
|
||||||
state.sourceNote = defaultSourceNote;
|
state.sourceNote = defaultSourceNote;
|
||||||
|
resetInlineEditors();
|
||||||
}
|
}
|
||||||
|
|
||||||
function bind() {
|
function bind() {
|
||||||
@@ -480,6 +646,7 @@ export function createIngredientCardController({ idBase, defaultSourceNote = 'Ze
|
|||||||
el('close')?.addEventListener('click', close);
|
el('close')?.addEventListener('click', close);
|
||||||
el('back')?.addEventListener('click', () => {
|
el('back')?.addEventListener('click', () => {
|
||||||
state.productId = null;
|
state.productId = null;
|
||||||
|
resetInlineEditors();
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
el('overlay')?.addEventListener('click', (event) => {
|
el('overlay')?.addEventListener('click', (event) => {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
renderCalendarGrid,
|
renderCalendarGrid,
|
||||||
syncCalendarTodayButton,
|
syncCalendarTodayButton,
|
||||||
} from './mealCalendar.js?v=1';
|
} from './mealCalendar.js?v=1';
|
||||||
import { createIngredientCardController, getIngredientCardHTML } from './ingredientCard.js?v=20260410-99';
|
import { createIngredientCardController, getIngredientCardHTML } from './ingredientCard.js?v=20260410-106';
|
||||||
|
|
||||||
function esc(s) {
|
function esc(s) {
|
||||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
loadPantry,
|
loadPantry,
|
||||||
getPantryTotal,
|
getPantryTotal,
|
||||||
} from '../services/pantryShopping.js?v=2';
|
} from '../services/pantryShopping.js?v=2';
|
||||||
import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260410-99';
|
import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260410-106';
|
||||||
|
|
||||||
/* ── helpers ── */
|
/* ── helpers ── */
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user