import { INGREDIENTS, CATEGORY_LABELS } from '../data/catalog.js'; import { PANTRY_STORAGE_KEY, SHOPPING_STORAGE_KEY } from '../storageKeys.js'; export const KITCHEN_LIST_ID = 'kitchen'; export const MISC_LIST_ID = 'misc'; function newId(prefix) { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID(); } return `${prefix}${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; } /** @typedef {{ id: string, ingredientId: string, name: string, amount: number, unit: string, category: string, checked: boolean, sourceNote?: string }} KitchenShoppingItem */ /** @typedef {{ id: string, text: string, note?: string, checked: boolean }} FreeformShoppingItem */ /** @typedef {{ id: string, name: string, type: 'kitchen'|'freeform', items: (KitchenShoppingItem|FreeformShoppingItem)[] }} ShoppingListDef */ /** * @typedef {{ * lists: ShoppingListDef[], * activeListId: string * }} ShoppingState */ function defaultShoppingState() { return { lists: [ { id: KITCHEN_LIST_ID, name: 'Kuchnia', type: 'kitchen', items: [] }, { id: MISC_LIST_ID, name: 'Inne zakupy', type: 'freeform', items: [] }, ], activeListId: KITCHEN_LIST_ID, }; } /** @param {unknown} x */ function normalizeKitchenItem(x) { if (!x || typeof x !== 'object') return null; const o = /** @type {Record} */ (x); if (!o.ingredientId || !Number.isFinite(Number(o.amount))) return null; const id = String(o.id && String(o.id).length ? o.id : newId('s')); const ingId = String(o.ingredientId); return { id, ingredientId: ingId, name: String(o.name || INGREDIENTS[ingId]?.name || ingId), amount: Math.round(Number(o.amount) * 100) / 100, unit: String(o.unit || ''), category: String(o.category || INGREDIENTS[ingId]?.category || 'inne'), checked: Boolean(o.checked), sourceNote: o.sourceNote ? String(o.sourceNote) : undefined, }; } /** @param {unknown} x */ function normalizeFreeformItem(x) { if (!x || typeof x !== 'object') return null; const o = /** @type {Record} */ (x); if (!o.text || !String(o.text).trim()) return null; return { id: String(o.id && String(o.id).length ? o.id : newId('f')), text: String(o.text).trim(), note: o.note ? String(o.note) : undefined, checked: Boolean(o.checked), }; } /** @returns {ShoppingState} */ function normalizeShoppingState(raw) { const base = defaultShoppingState(); if (!raw || typeof raw !== 'object') return base; const p = /** @type {Record} */ (raw); if (Array.isArray(p)) { const items = p.map(normalizeKitchenItem).filter(Boolean); const kitchen = base.lists.find((l) => l.id === KITCHEN_LIST_ID); if (kitchen && kitchen.type === 'kitchen') { kitchen.items = /** @type {KitchenShoppingItem[]} */ (items); } return base; } if (!Array.isArray(p.lists)) return base; /** @type {ShoppingListDef[]} */ const lists = []; for (const L of p.lists) { if (!L || typeof L !== 'object') continue; const row = /** @type {Record} */ (L); const id = String(row.id || ''); const name = String(row.name || 'Lista'); let type = row.type === 'freeform' ? 'freeform' : 'kitchen'; if (id === KITCHEN_LIST_ID) type = 'kitchen'; if (id === MISC_LIST_ID) type = 'freeform'; if (!id) continue; if (type === 'kitchen') { const items = Array.isArray(row.items) ? row.items.map(normalizeKitchenItem).filter(Boolean) : []; lists.push({ id, name, type: 'kitchen', items: /** @type {KitchenShoppingItem[]} */ (items) }); } else { const items = Array.isArray(row.items) ? row.items.map(normalizeFreeformItem).filter(Boolean) : []; lists.push({ id, name, type: 'freeform', items: /** @type {FreeformShoppingItem[]} */ (items) }); } } if (lists.length === 0) return base; const hasKitchen = lists.some((l) => l.id === KITCHEN_LIST_ID); if (!hasKitchen) { lists.unshift(/** @type {ShoppingListDef} */ (base.lists[0])); } const hasMisc = lists.some((l) => l.id === MISC_LIST_ID); if (!hasMisc) { lists.push(/** @type {ShoppingListDef} */ (base.lists[1])); } let activeListId = String(p.activeListId || KITCHEN_LIST_ID); if (!lists.some((l) => l.id === activeListId)) activeListId = lists[0].id; return { lists, activeListId }; } export function loadShoppingState() { try { const raw = localStorage.getItem(SHOPPING_STORAGE_KEY); if (!raw) return defaultShoppingState(); const parsed = JSON.parse(raw); return normalizeShoppingState(parsed); } catch { return defaultShoppingState(); } } export function saveShoppingState(state) { try { localStorage.setItem(SHOPPING_STORAGE_KEY, JSON.stringify(state)); } catch { /* ignore */ } } export function getListSummaries() { const { lists, activeListId } = loadShoppingState(); return { lists: lists.map((l) => ({ id: l.id, name: l.name, type: l.type, openCount: l.items.filter((i) => !i.checked).length, })), activeListId, }; } export function setActiveListId(listId) { const s = loadShoppingState(); if (!s.lists.some((l) => l.id === listId)) return s; s.activeListId = listId; saveShoppingState(s); return s; } export function getListById(listId) { return loadShoppingState().lists.find((l) => l.id === listId) ?? null; } export function getActiveList() { const s = loadShoppingState(); return s.lists.find((l) => l.id === s.activeListId) ?? s.lists[0]; } /** * @param {string} name * @returns {string} new list id */ export function addFreeformList(name) { const s = loadShoppingState(); const id = newId('list'); s.lists.push({ id, name: name.trim() || 'Nowa lista', type: 'freeform', items: [], }); s.activeListId = id; saveShoppingState(s); return id; } /** * @param {string} listId * @returns {boolean} */ export function deleteList(listId) { if (listId === KITCHEN_LIST_ID) return false; const s = loadShoppingState(); const idx = s.lists.findIndex((l) => l.id === listId); if (idx < 0) return false; s.lists.splice(idx, 1); if (s.activeListId === listId) { s.activeListId = s.lists[0]?.id || KITCHEN_LIST_ID; } saveShoppingState(s); return true; } /** * Dodaje linie tylko do listy kuchennej (składniki z katalogu). * @param {{ ingredientId: string, amount: number, unit: string, name?: string, category?: string, sourceNote?: string }[]} lines * @param {string} [listId] */ export function addOrMergeShoppingLines(lines, listId = KITCHEN_LIST_ID) { const s = loadShoppingState(); const list = s.lists.find((l) => l.id === listId && l.type === 'kitchen'); if (!list || list.type !== 'kitchen') return s; const items = /** @type {KitchenShoppingItem[]} */ (list.items); const open = items.filter((x) => !x.checked); const closed = items.filter((x) => x.checked); for (const L of lines) { const def = INGREDIENTS[L.ingredientId]; const name = L.name || def?.name || L.ingredientId; const category = L.category || def?.category || 'inne'; const idx = open.findIndex( (x) => x.ingredientId === L.ingredientId && x.unit === L.unit, ); if (idx >= 0) { open[idx].amount = Math.round((open[idx].amount + L.amount) * 100) / 100; if (L.sourceNote && !open[idx].sourceNote) open[idx].sourceNote = L.sourceNote; } else { open.push({ id: newId('s'), ingredientId: L.ingredientId, name, amount: Math.round(L.amount * 100) / 100, unit: L.unit, category, checked: false, sourceNote: L.sourceNote, }); } } list.items = [...open, ...closed]; saveShoppingState(s); return s; } /** * 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, sourceNote) { const def = INGREDIENTS[ingredientId]; if (!def) return; const unit = displayUnit(def.pantryUnit); addOrMergeShoppingLines([{ ingredientId, amount, unit, name: def.name, category: def.category, sourceNote: sourceNote ?? 'Ze spiżarni', }], KITCHEN_LIST_ID); } /** * @param {string} listId * @param {string} text * @param {string} [note] */ export function addFreeformLine(listId, text, note) { const t = text.trim(); if (!t) return; const s = loadShoppingState(); const list = s.lists.find((l) => l.id === listId && l.type === 'freeform'); if (!list) return; const items = /** @type {FreeformShoppingItem[]} */ (list.items); items.push({ id: newId('f'), text: t, note: note?.trim() || undefined, checked: false, }); saveShoppingState(s); } export function toggleItemInList(listId, itemId) { const s = loadShoppingState(); const list = s.lists.find((l) => l.id === listId); if (!list) return s; const it = list.items.find((i) => i.id === itemId); if (it) it.checked = !it.checked; saveShoppingState(s); return s; } export function removeItemFromList(listId, itemId) { const s = loadShoppingState(); const list = s.lists.find((l) => l.id === listId); if (!list) return s; list.items = list.items.filter((i) => i.id !== itemId); saveShoppingState(s); return s; } export function clearCheckedInList(listId) { const s = loadShoppingState(); const list = s.lists.find((l) => l.id === listId); if (!list) return s; list.items = list.items.filter((i) => !i.checked); saveShoppingState(s); return s; } export function computeShortfalls(needLines, pantry = loadPantry()) { const short = []; for (const L of needLines) { const def = INGREDIENTS[L.ingredientId]; if (!def) continue; const u = normalizeUnitForPantry(L.unit, def.pantryUnit); if (u === null) { short.push({ ...L, pantryQty: 0, shortfall: L.amount, unitMismatch: true }); continue; } const have = Number(pantry[L.ingredientId]) || 0; const miss = Math.max(0, Math.round((L.amount - have) * 100) / 100); if (miss > 0) { short.push({ ...L, unit: displayUnit(def.pantryUnit), pantryQty: have, shortfall: miss, unitMismatch: false, }); } } return short; } function displayUnit(pantryUnit) { if (pantryUnit === 'szt') return 'szt.'; return pantryUnit; } function normalizeUnitForPantry(recipeUnit, pantryUnit) { const r = String(recipeUnit).trim().toLowerCase().replace(/\.$/, ''); if (pantryUnit === 'g' && (r === 'g' || r === 'gram')) return 'g'; if (pantryUnit === 'ml' && (r === 'ml' || r === 'mililitr')) return 'ml'; if (pantryUnit === 'szt' && (r === 'szt' || r === 'sztuka' || r === 'kromki' || r === 'kromka')) return 'szt'; return null; } export function categoryLabel(cat) { return CATEGORY_LABELS[cat] || CATEGORY_LABELS.inne; } /** @returns {Record} ingredientId -> ilość w pantryUnit */ export function loadPantry() { try { const raw = localStorage.getItem(PANTRY_STORAGE_KEY); if (!raw) return {}; const p = JSON.parse(raw); if (typeof p !== 'object' || p === null) return {}; const out = {}; Object.keys(p).forEach((k) => { const n = Number(p[k]); if (!Number.isFinite(n) || n < 0) return; out[k] = n; }); return out; } catch { return {}; } } export function savePantry(pantry) { try { localStorage.setItem(PANTRY_STORAGE_KEY, JSON.stringify(pantry)); } catch { /* ignore */ } } export function setPantryQty(ingredientId, qty) { const pantry = loadPantry(); if (qty <= 0 || !Number.isFinite(qty)) delete pantry[ingredientId]; else pantry[ingredientId] = Math.round(qty * 1000) / 1000; savePantry(pantry); return pantry; } /** Kupione ze listy kuchennej → spiżarnia (zawsze ta lista, niezależnie od aktywnej zakładki). */ export function applyCheckedKitchenListToPantry() { const s = loadShoppingState(); const kitchen = s.lists.find((l) => l.id === KITCHEN_LIST_ID && l.type === 'kitchen'); if (!kitchen) return { pantry: loadPantry(), state: s }; const pantry = loadPantry(); const items = /** @type {KitchenShoppingItem[]} */ (kitchen.items); for (const it of items) { if (!it.checked) continue; const def = INGREDIENTS[it.ingredientId]; if (!def) continue; if (normalizeUnitForPantry(it.unit, def.pantryUnit) === null) continue; const cur = Number(pantry[it.ingredientId]) || 0; pantry[it.ingredientId] = Math.round((cur + it.amount) * 1000) / 1000; } kitchen.items = items.filter((i) => !i.checked); savePantry(pantry); saveShoppingState(s); return { pantry, state: s }; }