Improve pantry view
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Katalog składników i przepisów — odpowiednik tabel w DB (edycja poza aplikacją).
|
* Katalog składników i przepisów — odpowiednik tabel w DB (edycja poza aplikacją).
|
||||||
* pantryUnit: jednostka magazynowa / sumowania na liście zakupów (g, ml, szt.).
|
* pantryUnit: jednostka magazynowa / sumowania na liście zakupów (g, ml, szt.).
|
||||||
|
* purchasePack: minimalna „sztuka” ze sklepu w tej samej jednostce co pantryUnit (np. 200 g).
|
||||||
* nutritionPer100g — wartości szacunkowe na 100 g (dla płynów: traktuj ml≈g przy wodzie).
|
* nutritionPer100g — wartości szacunkowe na 100 g (dla płynów: traktuj ml≈g przy wodzie).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -15,7 +16,13 @@ export const CATEGORY_LABELS = {
|
|||||||
inne: 'Inne',
|
inne: 'Inne',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {Record<string, { id: string, name: string, category: keyof typeof CATEGORY_LABELS, pantryUnit: 'g'|'ml'|'szt', nutritionPer100g?: { kcal: number, protein: number, fat: number, carbs: number } }>} */
|
/**
|
||||||
|
* @typedef {{ kcal: number, protein: number, fat: number, carbs: number }} NutritionPer100
|
||||||
|
* @typedef {{ amount: number, label?: string }} PurchasePack
|
||||||
|
* @typedef {{ id: string, name: string, category: keyof typeof CATEGORY_LABELS, pantryUnit: 'g'|'ml'|'szt', purchasePack?: PurchasePack, nutritionPer100g?: NutritionPer100 }} IngredientDef
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {Record<string, IngredientDef>} */
|
||||||
export const INGREDIENTS = {
|
export const INGREDIENTS = {
|
||||||
maka_pszenna: {
|
maka_pszenna: {
|
||||||
id: 'maka_pszenna',
|
id: 'maka_pszenna',
|
||||||
@@ -29,6 +36,7 @@ export const INGREDIENTS = {
|
|||||||
name: 'Mleko',
|
name: 'Mleko',
|
||||||
category: 'nabial',
|
category: 'nabial',
|
||||||
pantryUnit: 'ml',
|
pantryUnit: 'ml',
|
||||||
|
purchasePack: { amount: 1000, label: 'butelka 1 l' },
|
||||||
nutritionPer100g: { kcal: 42, protein: 3.4, fat: 1, carbs: 5 },
|
nutritionPer100g: { kcal: 42, protein: 3.4, fat: 1, carbs: 5 },
|
||||||
},
|
},
|
||||||
jajko: {
|
jajko: {
|
||||||
@@ -169,6 +177,7 @@ export const INGREDIENTS = {
|
|||||||
name: 'Serek wiejski',
|
name: 'Serek wiejski',
|
||||||
category: 'nabial',
|
category: 'nabial',
|
||||||
pantryUnit: 'g',
|
pantryUnit: 'g',
|
||||||
|
purchasePack: { amount: 200, label: 'opakowanie 200 g' },
|
||||||
nutritionPer100g: { kcal: 97, protein: 11, fat: 5, carbs: 3 },
|
nutritionPer100g: { kcal: 97, protein: 11, fat: 5, carbs: 3 },
|
||||||
},
|
},
|
||||||
orzechy_wloskie: {
|
orzechy_wloskie: {
|
||||||
@@ -316,3 +325,47 @@ export const RECIPES = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Krok +/- w spiżarni: całe opakowanie albo domyślny krok (10 g/ml lub 1 szt.).
|
||||||
|
* @param {string} ingredientId
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function pantryQtyStep(ingredientId) {
|
||||||
|
const d = INGREDIENTS[ingredientId];
|
||||||
|
if (!d) return 10;
|
||||||
|
if (d.purchasePack && Number.isFinite(d.purchasePack.amount) && d.purchasePack.amount > 0) {
|
||||||
|
return d.purchasePack.amount;
|
||||||
|
}
|
||||||
|
return d.pantryUnit === 'szt' ? 1 : 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {IngredientDef} def
|
||||||
|
* @param {number} stockQty — w pantryUnit
|
||||||
|
*/
|
||||||
|
export function nutritionForStock(def, stockQty) {
|
||||||
|
const n = def.nutritionPer100g;
|
||||||
|
if (!n || !Number.isFinite(stockQty) || stockQty <= 0) return null;
|
||||||
|
const f = stockQty / 100;
|
||||||
|
return {
|
||||||
|
kcal: Math.round(n.kcal * f),
|
||||||
|
protein: Math.round(n.protein * f * 10) / 10,
|
||||||
|
fat: Math.round(n.fat * f * 10) / 10,
|
||||||
|
carbs: Math.round(n.carbs * f * 10) / 10,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pełne opakowania + reszta (np. 450 g / 200 → 2 + 50 g).
|
||||||
|
* @param {IngredientDef} def
|
||||||
|
* @param {number} stockQty
|
||||||
|
* @returns {{ fullPacks: number, remainder: number } | null}
|
||||||
|
*/
|
||||||
|
export function splitStockIntoPacks(def, stockQty) {
|
||||||
|
const size = def.purchasePack?.amount;
|
||||||
|
if (!size || !Number.isFinite(size) || size <= 0 || !Number.isFinite(stockQty)) return null;
|
||||||
|
const fullPacks = Math.floor(stockQty / size);
|
||||||
|
const remainder = Math.round((stockQty - fullPacks * size) * 10) / 10;
|
||||||
|
return { fullPacks, remainder };
|
||||||
|
}
|
||||||
|
|||||||
@@ -248,9 +248,10 @@ export function addOrMergeShoppingLines(lines, listId = KITCHEN_LIST_ID) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Jedna sztuka / domyślna jednostka magazynowa — na listę kuchenną ze spiżarni.
|
* Na listę kuchenną ze spiżarni (amount w pantryUnit: g, ml lub szt.).
|
||||||
|
* @param {string} [sourceNote] — nadpisuje domyślne „Ze spiżarni”
|
||||||
*/
|
*/
|
||||||
export function addIngredientToKitchenList(ingredientId, amount = 1) {
|
export function addIngredientToKitchenList(ingredientId, amount = 1, sourceNote) {
|
||||||
const def = INGREDIENTS[ingredientId];
|
const def = INGREDIENTS[ingredientId];
|
||||||
if (!def) return;
|
if (!def) return;
|
||||||
const unit = displayUnit(def.pantryUnit);
|
const unit = displayUnit(def.pantryUnit);
|
||||||
@@ -260,7 +261,7 @@ export function addIngredientToKitchenList(ingredientId, amount = 1) {
|
|||||||
unit,
|
unit,
|
||||||
name: def.name,
|
name: def.name,
|
||||||
category: def.category,
|
category: def.category,
|
||||||
sourceNote: 'Ze spiżarni',
|
sourceNote: sourceNote ?? 'Ze spiżarni',
|
||||||
}], KITCHEN_LIST_ID);
|
}], KITCHEN_LIST_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { INGREDIENTS, CATEGORY_LABELS } from '../data/catalog.js';
|
import {
|
||||||
|
INGREDIENTS,
|
||||||
|
CATEGORY_LABELS,
|
||||||
|
pantryQtyStep,
|
||||||
|
splitStockIntoPacks,
|
||||||
|
} from '../data/catalog.js';
|
||||||
import { addIngredientToKitchenList, categoryLabel, loadPantry, setPantryQty } from '../services/pantryShopping.js';
|
import { addIngredientToKitchenList, categoryLabel, loadPantry, setPantryQty } from '../services/pantryShopping.js';
|
||||||
import { showAppToast } from '../ui/toast.js';
|
import { showAppToast } from '../ui/toast.js';
|
||||||
|
|
||||||
@@ -26,6 +31,8 @@ const PANTRY_SHOP_OFF = `translateY(calc(100% + ${PANTRY_SHOP_BOTTOM}))`;
|
|||||||
let shopPickerIngredientId = null;
|
let shopPickerIngredientId = null;
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
let shopPickerStep = 1;
|
let shopPickerStep = 1;
|
||||||
|
/** Czy licznik w arkuszu to liczba opakowań (vs. jednostki magazynowe). */
|
||||||
|
let shopPickerUsesPacks = false;
|
||||||
|
|
||||||
export function getPantryHTML() {
|
export function getPantryHTML() {
|
||||||
return `
|
return `
|
||||||
@@ -66,6 +73,31 @@ let pantryFilterCategory = '';
|
|||||||
let pantryAccordionHaveOpen = true;
|
let pantryAccordionHaveOpen = true;
|
||||||
let pantryAccordionCatalogOpen = false;
|
let pantryAccordionCatalogOpen = false;
|
||||||
|
|
||||||
|
/** Po zmianie ilości przez próg 0 ↔ zapas karta zostaje wizualnie w tej samej sekcji przez chwilę. */
|
||||||
|
const PANTRY_SECTION_PIN_MS = 1400;
|
||||||
|
|
||||||
|
/** @type {Record<string, { section: 'have'|'catalog', until: number }>} */
|
||||||
|
const pantrySectionPins = {};
|
||||||
|
/** @type {Record<string, ReturnType<typeof setTimeout>>} */
|
||||||
|
const pantryPinTimers = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @param {'have'|'catalog'} section
|
||||||
|
*/
|
||||||
|
function pinPantrySection(id, section) {
|
||||||
|
pantrySectionPins[id] = { section, until: Date.now() + PANTRY_SECTION_PIN_MS };
|
||||||
|
if (pantryPinTimers[id]) {
|
||||||
|
clearTimeout(pantryPinTimers[id]);
|
||||||
|
delete pantryPinTimers[id];
|
||||||
|
}
|
||||||
|
pantryPinTimers[id] = setTimeout(() => {
|
||||||
|
delete pantrySectionPins[id];
|
||||||
|
delete pantryPinTimers[id];
|
||||||
|
renderPantryResults();
|
||||||
|
}, PANTRY_SECTION_PIN_MS);
|
||||||
|
}
|
||||||
|
|
||||||
function allCategoryKeys() {
|
function allCategoryKeys() {
|
||||||
const s = new Set();
|
const s = new Set();
|
||||||
Object.values(INGREDIENTS).forEach((d) => s.add(d.category));
|
Object.values(INGREDIENTS).forEach((d) => s.add(d.category));
|
||||||
@@ -113,15 +145,93 @@ function filterIds(searchRaw) {
|
|||||||
.sort((a, b) => INGREDIENTS[a].name.localeCompare(INGREDIENTS[b].name, 'pl'));
|
.sort((a, b) => INGREDIENTS[a].name.localeCompare(INGREDIENTS[b].name, 'pl'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Krok +/-: tylko liczby całkowite (szt. ±1, g/ml ±10). */
|
function packStockCaption(def, stockQty) {
|
||||||
function qtyStepForIngredient(id) {
|
const split = splitStockIntoPacks(def, stockQty);
|
||||||
const u = INGREDIENTS[id]?.pantryUnit;
|
if (!split || !def.purchasePack) return '';
|
||||||
return u === 'szt' ? 1 : 10;
|
const u = pantryUnitLabel(def.pantryUnit);
|
||||||
|
const hint = def.purchasePack.label || `${def.purchasePack.amount} ${u}`;
|
||||||
|
const { fullPacks, remainder } = split;
|
||||||
|
if (fullPacks <= 0 && remainder <= 0) {
|
||||||
|
return `<span class="text-[10px] text-gray-500 block mt-1">Kupujesz w: ${escapeHtml(hint)}</span>`;
|
||||||
|
}
|
||||||
|
const bits = [];
|
||||||
|
if (fullPacks > 0) bits.push(`${fullPacks}× opak.`);
|
||||||
|
if (remainder > 0) bits.push(`+ ${remainder} ${u}`);
|
||||||
|
return `<span class="text-[10px] text-gray-600 block mt-1">${escapeHtml(bits.join(' '))} <span class="text-gray-400">(${escapeHtml(hint)})</span></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mała ikona w prawym górnym rogu karty — rozwija panel w dół. */
|
||||||
|
function nutritionCornerToggle(ingredientId) {
|
||||||
|
const panelId = `pantry-nut-${ingredientId}`;
|
||||||
|
return `
|
||||||
|
<button type="button"
|
||||||
|
class="pantry-nutrition-toggle shrink-0 -mr-0.5 -mt-0.5 flex h-8 w-8 items-center justify-center rounded-lg text-gray-400 hover:text-gray-700 hover:bg-white/90 active:bg-white border border-transparent hover:border-gray-200/70 transition-colors"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="${escapeHtml(panelId)}"
|
||||||
|
title="Wartości odżywcze"
|
||||||
|
aria-label="Pokaż lub ukryj wartości odżywcze">
|
||||||
|
<i class="fas fa-chevron-down pantry-nutrition-chevron text-[11px] transition-transform duration-200" aria-hidden="true"></i>
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nutritionListRow(label, valueHtml) {
|
||||||
|
return `<li class="flex items-baseline justify-between gap-3 py-0.5 border-b border-gray-100/80 last:border-0">
|
||||||
|
<span class="text-gray-500 shrink-0">${escapeHtml(label)}</span>
|
||||||
|
<span class="text-right font-semibold tabular-nums text-gray-800">${valueHtml}</span>
|
||||||
|
</li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nutritionPanelHtml(def, ingredientId) {
|
||||||
|
const n = def.nutritionPer100g;
|
||||||
|
if (!n) return '';
|
||||||
|
const panelId = `pantry-nut-${ingredientId}`;
|
||||||
|
const refLabel = def.pantryUnit === 'ml' ? '100 ml produktu' : '100 g produktu';
|
||||||
|
|
||||||
|
const refList = `
|
||||||
|
<ul class="space-y-0 rounded-lg bg-white/70 px-2 py-1 ring-1 ring-gray-100/90">
|
||||||
|
${nutritionListRow('Energia', `${n.kcal} kcal`)}
|
||||||
|
${nutritionListRow('Białko', `${n.protein} g`)}
|
||||||
|
${nutritionListRow('Tłuszcz', `${n.fat} g`)}
|
||||||
|
${nutritionListRow('Węglowodany', `${n.carbs} g`)}
|
||||||
|
</ul>`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div id="${escapeHtml(panelId)}"
|
||||||
|
class="pantry-nutrition-panel hidden text-[10px] leading-snug"
|
||||||
|
role="region"
|
||||||
|
aria-label="Wartości odżywcze">
|
||||||
|
<div class="mt-2 pt-2.5 border-t border-gray-200/70 space-y-1">
|
||||||
|
<p class="text-[9px] font-semibold uppercase tracking-wide text-gray-500 px-0.5">${escapeHtml(refLabel)}</p>
|
||||||
|
${refList}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitHaveAndCatalog(ids, pantry) {
|
function splitHaveAndCatalog(ids, pantry) {
|
||||||
const have = ids.filter((id) => (Number(pantry[id]) || 0) > 0);
|
const now = Date.now();
|
||||||
const catalogOnly = ids.filter((id) => !pantry[id] || Number(pantry[id]) <= 0);
|
/** @type {string[]} */
|
||||||
|
const have = [];
|
||||||
|
/** @type {string[]} */
|
||||||
|
const catalogOnly = [];
|
||||||
|
|
||||||
|
for (const id of ids) {
|
||||||
|
const pin = pantrySectionPins[id];
|
||||||
|
if (pin && pin.until > now) {
|
||||||
|
if (pin.section === 'have') have.push(id);
|
||||||
|
else catalogOnly.push(id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (pin && pin.until <= now) {
|
||||||
|
delete pantrySectionPins[id];
|
||||||
|
if (pantryPinTimers[id]) {
|
||||||
|
clearTimeout(pantryPinTimers[id]);
|
||||||
|
delete pantryPinTimers[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const qty = Number(pantry[id]) || 0;
|
||||||
|
if (qty > 0) have.push(id);
|
||||||
|
else catalogOnly.push(id);
|
||||||
|
}
|
||||||
return { have, catalogOnly };
|
return { have, catalogOnly };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,28 +286,45 @@ function pantryCardHtml(id, pantry, variant) {
|
|||||||
const unit = pantryUnitLabel(def.pantryUnit);
|
const unit = pantryUnitLabel(def.pantryUnit);
|
||||||
const qty = Number(pantry[id]) || 0;
|
const qty = Number(pantry[id]) || 0;
|
||||||
const val = qty > 0 ? String(Math.round(qty)) : '';
|
const val = qty > 0 ? String(Math.round(qty)) : '';
|
||||||
const step = qtyStepForIngredient(id);
|
const step = pantryQtyStep(id);
|
||||||
|
const pack = def.purchasePack;
|
||||||
|
const packPill = pack
|
||||||
|
? `<span class="shrink-0 inline-flex items-center px-2 py-0.5 rounded-lg bg-violet-100 text-violet-800 text-[10px] font-bold">${escapeHtml(pack.label || `${pack.amount} ${unit}`)}</span>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const stepHint = pack
|
||||||
|
? `+/−: ${step} ${unit} (1 opak.)`
|
||||||
|
: `+/−: ${step} ${unit}`;
|
||||||
|
|
||||||
const shell = variant === 'have'
|
const shell = variant === 'have'
|
||||||
? 'rounded-xl border border-emerald-200 bg-gradient-to-br from-emerald-50/80 to-white p-3 shadow-sm ring-1 ring-emerald-100/80'
|
? 'rounded-xl border border-emerald-200 bg-gradient-to-br from-emerald-50/80 to-white p-3 shadow-sm ring-1 ring-emerald-100/80'
|
||||||
: 'rounded-xl border border-dashed border-gray-200 bg-gray-50/90 p-3 shadow-sm';
|
: 'rounded-xl border border-dashed border-gray-200 bg-gray-50/90 p-3 shadow-sm';
|
||||||
|
|
||||||
|
const hasNutrition = Boolean(def.nutritionPer100g);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="${shell}" data-ingredient-id="${escapeHtml(id)}" data-pantry-variant="${variant}">
|
<div class="${shell}" data-ingredient-id="${escapeHtml(id)}" data-pantry-variant="${variant}">
|
||||||
<div class="flex items-start justify-between gap-2 mb-2">
|
<div class="flex items-start justify-between gap-1 mb-1">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0 flex-1 pr-1">
|
||||||
|
<div class="flex items-start gap-2 flex-wrap">
|
||||||
<p class="text-sm font-semibold text-gray-900">${escapeHtml(def.name)}</p>
|
<p class="text-sm font-semibold text-gray-900">${escapeHtml(def.name)}</p>
|
||||||
<p class="text-[10px] text-gray-500 mt-0.5">${escapeHtml(categoryLabel(def.category))} · magazyn: ${unit}</p>
|
${packPill}
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-[10px] text-gray-500 mt-0.5">${escapeHtml(categoryLabel(def.category))} · stan w ${unit}</p>
|
||||||
|
${packStockCaption(def, qty)}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
${hasNutrition ? nutritionCornerToggle(id) : ''}
|
||||||
|
</div>
|
||||||
|
${hasNutrition ? nutritionPanelHtml(def, id) : ''}
|
||||||
|
<div class="flex flex-wrap items-center gap-2 mt-2.5">
|
||||||
<div class="flex items-center gap-1 ${variant === 'have' ? 'bg-emerald-100/60' : 'bg-gray-100'} rounded-lg p-0.5">
|
<div class="flex items-center gap-1 ${variant === 'have' ? 'bg-emerald-100/60' : 'bg-gray-100'} rounded-lg p-0.5">
|
||||||
<button type="button" class="pantry-qty-minus w-8 h-8 rounded-md bg-white shadow-sm text-gray-700 hover:text-gray-900 flex items-center justify-center" aria-label="Mniej"><i class="fas fa-minus text-[10px]"></i></button>
|
<button type="button" class="pantry-qty-minus w-8 h-8 rounded-md bg-white shadow-sm text-gray-700 hover:text-gray-900 flex items-center justify-center" aria-label="Mniej"><i class="fas fa-minus text-[10px]"></i></button>
|
||||||
<input type="number" min="0" step="1" inputmode="numeric" pattern="[0-9]*" data-pantry-qty data-pantry-step="${step}"
|
<input type="number" min="0" step="1" inputmode="decimal" data-pantry-qty data-pantry-step="${step}"
|
||||||
class="w-16 text-center text-sm font-semibold tabular-nums bg-transparent border-0 outline-none py-1"
|
class="w-[4.25rem] text-center text-sm font-semibold tabular-nums bg-transparent border-0 outline-none py-1"
|
||||||
value="${val}" placeholder="0" title="Wpisz liczbę całkowitą; +/− zmienia o ${step} ${unit}" />
|
value="${val}" placeholder="0" title="${escapeHtml(stepHint)}" />
|
||||||
<button type="button" class="pantry-qty-plus w-8 h-8 rounded-md bg-white shadow-sm text-gray-700 hover:text-gray-900 flex items-center justify-center" aria-label="Więcej"><i class="fas fa-plus text-[10px]"></i></button>
|
<button type="button" class="pantry-qty-plus w-8 h-8 rounded-md bg-white shadow-sm text-gray-700 hover:text-gray-900 flex items-center justify-center" aria-label="Więcej"><i class="fas fa-plus text-[10px]"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="text-[10px] text-gray-400">${escapeHtml(stepHint)}</span>
|
||||||
<button type="button" class="pantry-add-shop ml-auto flex items-center gap-1.5 px-3 py-2 rounded-lg bg-gray-900 text-white text-xs font-semibold hover:bg-black transition-colors">
|
<button type="button" class="pantry-add-shop ml-auto flex items-center gap-1.5 px-3 py-2 rounded-lg bg-gray-900 text-white text-xs font-semibold hover:bg-black transition-colors">
|
||||||
<i class="fas fa-cart-plus text-[10px]"></i>
|
<i class="fas fa-cart-plus text-[10px]"></i>
|
||||||
Na listę…
|
Na listę…
|
||||||
@@ -278,10 +405,10 @@ function renderPantryResults() {
|
|||||||
const id = card.getAttribute('data-ingredient-id');
|
const id = card.getAttribute('data-ingredient-id');
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
const input = card.querySelector('[data-pantry-qty]');
|
const input = card.querySelector('[data-pantry-qty]');
|
||||||
const step = parseInt(String(input?.getAttribute('data-pantry-step')), 10) || qtyStepForIngredient(id);
|
const step = parseFloat(String(input?.getAttribute('data-pantry-step'))) || pantryQtyStep(id);
|
||||||
|
|
||||||
const applyQty = (n) => {
|
const applyQty = (n) => {
|
||||||
const v = Math.max(0, Math.round(Number(n)) || 0);
|
const v = Math.max(0, Math.round(Number(n) * 1000) / 1000 || 0);
|
||||||
setPantryQty(id, v);
|
setPantryQty(id, v);
|
||||||
if (input) {
|
if (input) {
|
||||||
input.value = v > 0 ? String(v) : '';
|
input.value = v > 0 ? String(v) : '';
|
||||||
@@ -289,39 +416,61 @@ function renderPantryResults() {
|
|||||||
const prevVariant = card.getAttribute('data-pantry-variant');
|
const prevVariant = card.getAttribute('data-pantry-variant');
|
||||||
const nowHave = v > 0;
|
const nowHave = v > 0;
|
||||||
const expectVariant = nowHave ? 'have' : 'catalog';
|
const expectVariant = nowHave ? 'have' : 'catalog';
|
||||||
|
if (prevVariant === 'have' || prevVariant === 'catalog') {
|
||||||
if (prevVariant !== expectVariant) {
|
if (prevVariant !== expectVariant) {
|
||||||
|
const existing = pantrySectionPins[id];
|
||||||
|
const t = Date.now();
|
||||||
|
const extending = Boolean(
|
||||||
|
existing && existing.until > t && existing.section === prevVariant,
|
||||||
|
);
|
||||||
|
pinPantrySection(id, prevVariant);
|
||||||
|
if (!extending) {
|
||||||
renderPantryResults();
|
renderPantryResults();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
card.querySelector('.pantry-qty-minus')?.addEventListener('click', () => {
|
card.querySelector('.pantry-qty-minus')?.addEventListener('click', () => {
|
||||||
const cur = Math.round(parseFloat(String(input?.value).replace(',', '.')) || 0);
|
const cur = parseFloat(String(input?.value).replace(',', '.')) || 0;
|
||||||
applyQty(Math.max(0, cur - step));
|
applyQty(Math.max(0, cur - step));
|
||||||
});
|
});
|
||||||
card.querySelector('.pantry-qty-plus')?.addEventListener('click', () => {
|
card.querySelector('.pantry-qty-plus')?.addEventListener('click', () => {
|
||||||
const cur = Math.round(parseFloat(String(input?.value).replace(',', '.')) || 0);
|
const cur = parseFloat(String(input?.value).replace(',', '.')) || 0;
|
||||||
applyQty(cur + step);
|
applyQty(cur + step);
|
||||||
});
|
});
|
||||||
input?.addEventListener('change', () => {
|
input?.addEventListener('change', () => {
|
||||||
const raw = String(input.value).replace(',', '.').trim();
|
const raw = String(input.value).replace(',', '.').trim();
|
||||||
const v = raw === '' ? 0 : Math.round(parseFloat(raw));
|
const v = raw === '' ? 0 : parseFloat(raw);
|
||||||
applyQty(Number.isFinite(v) ? v : 0);
|
applyQty(Number.isFinite(v) ? v : 0);
|
||||||
});
|
});
|
||||||
card.querySelector('.pantry-add-shop')?.addEventListener('click', () => {
|
card.querySelector('.pantry-add-shop')?.addEventListener('click', () => {
|
||||||
openPantryShopPicker(id);
|
openPantryShopPicker(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
card.querySelector('.pantry-nutrition-toggle')?.addEventListener('click', (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
const btn = /** @type {HTMLButtonElement} */ (ev.currentTarget);
|
||||||
|
const panel = card.querySelector('.pantry-nutrition-panel');
|
||||||
|
const chevron = card.querySelector('.pantry-nutrition-chevron');
|
||||||
|
if (!panel) return;
|
||||||
|
const willOpen = panel.classList.contains('hidden');
|
||||||
|
panel.classList.toggle('hidden', !willOpen);
|
||||||
|
btn.setAttribute('aria-expanded', String(willOpen));
|
||||||
|
chevron?.classList.toggle('rotate-180', willOpen);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function readShopPickerQty() {
|
function readShopPickerQty() {
|
||||||
const el = document.getElementById('pantry-shop-qty');
|
const el = document.getElementById('pantry-shop-qty');
|
||||||
const n = Math.round(parseFloat(String(el?.value).replace(',', '.')) || 0);
|
const raw = parseFloat(String(el?.value).replace(',', '.')) || 0;
|
||||||
return Math.max(1, n);
|
return Math.max(1, Math.round(raw));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setShopPickerQtyDisplay(v) {
|
function setShopPickerQtyDisplay(v) {
|
||||||
const el = document.getElementById('pantry-shop-qty');
|
const el = document.getElementById('pantry-shop-qty');
|
||||||
if (el) el.value = String(Math.max(1, Math.round(v)));
|
if (el) el.value = String(Math.max(1, Math.round(Number(v))));
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPantryShopPicker(ingredientId) {
|
function openPantryShopPicker(ingredientId) {
|
||||||
@@ -329,16 +478,28 @@ function openPantryShopPicker(ingredientId) {
|
|||||||
if (!def) return;
|
if (!def) return;
|
||||||
|
|
||||||
shopPickerIngredientId = ingredientId;
|
shopPickerIngredientId = ingredientId;
|
||||||
shopPickerStep = qtyStepForIngredient(ingredientId);
|
|
||||||
const unit = pantryUnitLabel(def.pantryUnit);
|
const unit = pantryUnitLabel(def.pantryUnit);
|
||||||
|
const pack = def.purchasePack;
|
||||||
|
shopPickerUsesPacks = Boolean(pack && pack.amount > 0);
|
||||||
|
|
||||||
const heading = document.getElementById('pantry-shop-heading');
|
const heading = document.getElementById('pantry-shop-heading');
|
||||||
const sub = document.getElementById('pantry-shop-sub');
|
const sub = document.getElementById('pantry-shop-sub');
|
||||||
|
if (shopPickerUsesPacks) {
|
||||||
|
shopPickerStep = 1;
|
||||||
|
if (heading) heading.textContent = `Ile opakowań: ${def.name}?`;
|
||||||
|
if (sub) {
|
||||||
|
const lab = pack.label || `${pack.amount} ${unit}`;
|
||||||
|
sub.textContent = `Jedno = ${lab}. Na listę trafi suma w ${unit} (${lab}).`;
|
||||||
|
}
|
||||||
|
setShopPickerQtyDisplay(1);
|
||||||
|
} else {
|
||||||
|
shopPickerStep = pantryQtyStep(ingredientId);
|
||||||
if (heading) heading.textContent = `Ile dodać: ${def.name}?`;
|
if (heading) heading.textContent = `Ile dodać: ${def.name}?`;
|
||||||
if (sub) {
|
if (sub) {
|
||||||
sub.textContent = `Jednostka na liście: ${unit}. Przyciski +/−: ${shopPickerStep} ${unit}.`;
|
sub.textContent = `Jednostka na liście: ${unit}. Przyciski +/−: ${shopPickerStep} ${unit}.`;
|
||||||
}
|
}
|
||||||
setShopPickerQtyDisplay(shopPickerStep);
|
setShopPickerQtyDisplay(shopPickerStep);
|
||||||
|
}
|
||||||
|
|
||||||
const backdrop = document.getElementById('pantry-shop-backdrop');
|
const backdrop = document.getElementById('pantry-shop-backdrop');
|
||||||
const sheet = document.getElementById('pantry-shop-sheet');
|
const sheet = document.getElementById('pantry-shop-sheet');
|
||||||
@@ -354,6 +515,7 @@ function openPantryShopPicker(ingredientId) {
|
|||||||
|
|
||||||
function closePantryShopPicker() {
|
function closePantryShopPicker() {
|
||||||
shopPickerIngredientId = null;
|
shopPickerIngredientId = null;
|
||||||
|
shopPickerUsesPacks = false;
|
||||||
const backdrop = document.getElementById('pantry-shop-backdrop');
|
const backdrop = document.getElementById('pantry-shop-backdrop');
|
||||||
const sheet = document.getElementById('pantry-shop-sheet');
|
const sheet = document.getElementById('pantry-shop-sheet');
|
||||||
if (sheet) {
|
if (sheet) {
|
||||||
@@ -374,11 +536,13 @@ function bindPantryShopSheet() {
|
|||||||
|
|
||||||
document.getElementById('pantry-shop-minus')?.addEventListener('click', () => {
|
document.getElementById('pantry-shop-minus')?.addEventListener('click', () => {
|
||||||
const cur = readShopPickerQty();
|
const cur = readShopPickerQty();
|
||||||
setShopPickerQtyDisplay(Math.max(1, cur - shopPickerStep));
|
const dec = shopPickerUsesPacks ? 1 : shopPickerStep;
|
||||||
|
setShopPickerQtyDisplay(Math.max(1, cur - dec));
|
||||||
});
|
});
|
||||||
document.getElementById('pantry-shop-plus')?.addEventListener('click', () => {
|
document.getElementById('pantry-shop-plus')?.addEventListener('click', () => {
|
||||||
const cur = readShopPickerQty();
|
const cur = readShopPickerQty();
|
||||||
setShopPickerQtyDisplay(cur + shopPickerStep);
|
const inc = shopPickerUsesPacks ? 1 : shopPickerStep;
|
||||||
|
setShopPickerQtyDisplay(cur + inc);
|
||||||
});
|
});
|
||||||
document.getElementById('pantry-shop-qty')?.addEventListener('change', () => {
|
document.getElementById('pantry-shop-qty')?.addEventListener('change', () => {
|
||||||
setShopPickerQtyDisplay(readShopPickerQty());
|
setShopPickerQtyDisplay(readShopPickerQty());
|
||||||
@@ -386,9 +550,21 @@ function bindPantryShopSheet() {
|
|||||||
|
|
||||||
document.getElementById('pantry-shop-add')?.addEventListener('click', () => {
|
document.getElementById('pantry-shop-add')?.addEventListener('click', () => {
|
||||||
if (!shopPickerIngredientId) return;
|
if (!shopPickerIngredientId) return;
|
||||||
const qty = readShopPickerQty();
|
const def = INGREDIENTS[shopPickerIngredientId];
|
||||||
addIngredientToKitchenList(shopPickerIngredientId, qty);
|
if (!def) return;
|
||||||
showAppToast(`Dodano ${qty} na listę kuchni.`);
|
const count = readShopPickerQty();
|
||||||
|
const unit = pantryUnitLabel(def.pantryUnit);
|
||||||
|
|
||||||
|
if (shopPickerUsesPacks && def.purchasePack) {
|
||||||
|
const packAmt = def.purchasePack.amount;
|
||||||
|
const total = count * packAmt;
|
||||||
|
const note = `${count}× ${def.purchasePack.label || `${packAmt} ${unit}`}`;
|
||||||
|
addIngredientToKitchenList(shopPickerIngredientId, total, note);
|
||||||
|
showAppToast(`Dodano ${count} op. (${total} ${unit}) na listę kuchni.`);
|
||||||
|
} else {
|
||||||
|
addIngredientToKitchenList(shopPickerIngredientId, count);
|
||||||
|
showAppToast(`Dodano ${count} ${unit} na listę kuchni.`);
|
||||||
|
}
|
||||||
closePantryShopPicker();
|
closePantryShopPicker();
|
||||||
window.refreshShopping?.();
|
window.refreshShopping?.();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user