From 8e48ebdd95b7939f41ab4a1fb3885e5354fc7831 Mon Sep 17 00:00:00 2001 From: ulfrxdev Date: Fri, 17 Apr 2026 23:34:53 +0200 Subject: [PATCH] Redesign shopping list --- js/services/pantryShopping.js | 144 ++++++- js/services/planIngredients.js | 29 ++ js/storageKeys.js | 1 + js/ui/bottomNav.js | 1 - js/views/Pantry.js | 76 ++-- js/views/RecipeList.js | 2 +- js/views/ShoppingList.js | 766 +++++++++++++++++++++++++-------- 7 files changed, 793 insertions(+), 226 deletions(-) diff --git a/js/services/pantryShopping.js b/js/services/pantryShopping.js index 9ae5291..0466f4b 100644 --- a/js/services/pantryShopping.js +++ b/js/services/pantryShopping.js @@ -1,5 +1,5 @@ import { INGREDIENTS, CATEGORY_LABELS, PRODUCTS, ingredientHasProducts } from '../data/catalog.js?v=9'; -import { PANTRY_STORAGE_KEY, PANTRY_STORAGE_KEY_V2, SHOPPING_STORAGE_KEY } from '../storageKeys.js'; +import { PANTRY_STORAGE_KEY, PANTRY_STORAGE_KEY_V2, SHOPPING_STORAGE_KEY, SHOPPING_SESSION_KEY } from '../storageKeys.js'; export const KITCHEN_LIST_ID = 'kitchen'; export const MISC_LIST_ID = 'misc'; @@ -535,6 +535,148 @@ export function setPantryProductQty(ingredientId, productId, qty) { return pantry; } +/** + * Dodaj delta do spiżarni (używane przy zakupie ze listy). + * @param {string} ingredientId + * @param {string|undefined} productId + * @param {number} amount + */ +export function addAmountToPantry(ingredientId, productId, amount) { + const pantry = loadPantry(); + const rounded = Math.round(amount * 1000) / 1000; + if (productId && PRODUCTS[productId]) { + let val = pantry[ingredientId]; + if (!val || typeof val === 'number') { + val = { items: [], _total: 0 }; + pantry[ingredientId] = val; + } + const idx = val.items.findIndex((i) => i.productId === productId); + if (idx >= 0) val.items[idx].qty = Math.round((val.items[idx].qty + rounded) * 1000) / 1000; + else val.items.push({ productId, qty: rounded }); + recalcTotal(val); + } else { + const cur = typeof pantry[ingredientId] === 'number' ? pantry[ingredientId] : 0; + pantry[ingredientId] = Math.round((cur + rounded) * 1000) / 1000; + } + savePantry(pantry); + return pantry; +} + +/** + * Odejmij delta ze spiżarni (undo zakupu). + * @param {string} ingredientId + * @param {string|undefined} productId + * @param {number} amount + */ +export function subtractFromPantry(ingredientId, productId, amount) { + const pantry = loadPantry(); + if (productId && PRODUCTS[productId]) { + const val = pantry[ingredientId]; + if (!val || typeof val === 'number') return pantry; + const idx = val.items.findIndex((i) => i.productId === productId); + if (idx < 0) return pantry; + val.items[idx].qty = Math.max(0, Math.round((val.items[idx].qty - amount) * 1000) / 1000); + if (val.items[idx].qty <= 0) val.items.splice(idx, 1); + recalcTotal(val); + if (val._total <= 0) delete pantry[ingredientId]; + } else { + const cur = typeof pantry[ingredientId] === 'number' ? pantry[ingredientId] : 0; + const next = Math.max(0, Math.round((cur - amount) * 1000) / 1000); + if (next <= 0) delete pantry[ingredientId]; + else pantry[ingredientId] = next; + } + savePantry(pantry); + return pantry; +} + +/* ══════════════════════════════════════════════════════════════════════ + * Session zakupowy — selectedDays + log zakupów bieżącej sesji + * ══════════════════════════════════════════════════════════════════════ */ + +/** + * @typedef {{ id: string, ingredientId: string, productId?: string, name: string, addedAmount: number, unit: string, category: string, timestamp: number }} SessionLogEntry + * @typedef {{ selectedDays: string[], sessionLog: SessionLogEntry[] }} ShoppingSessionState + */ + +function defaultShoppingSession() { + return { selectedDays: [], sessionLog: [] }; +} + +function normalizeShoppingSession(raw) { + if (!raw || typeof raw !== 'object') return defaultShoppingSession(); + const selectedDays = Array.isArray(raw.selectedDays) + ? raw.selectedDays.filter((d) => typeof d === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(d)) + : []; + const sessionLog = Array.isArray(raw.sessionLog) + ? raw.sessionLog.filter((e) => e && typeof e.ingredientId === 'string' && Number.isFinite(e.addedAmount)) + : []; + return { selectedDays, sessionLog }; +} + +/** @returns {ShoppingSessionState} */ +export function loadShoppingSession() { + try { + const raw = localStorage.getItem(SHOPPING_SESSION_KEY); + if (!raw) return defaultShoppingSession(); + return normalizeShoppingSession(JSON.parse(raw)); + } catch { + return defaultShoppingSession(); + } +} + +/** @param {ShoppingSessionState} s */ +export function saveShoppingSession(s) { + try { + localStorage.setItem(SHOPPING_SESSION_KEY, JSON.stringify(s)); + } catch { /* ignore */ } +} + +/** @returns {string[]} */ +export function getSelectedDays() { + return loadShoppingSession().selectedDays; +} + +/** @param {string[]} days */ +export function setSelectedDays(days) { + const s = loadShoppingSession(); + s.selectedDays = days; + saveShoppingSession(s); +} + +/** + * @param {{ ingredientId: string, productId?: string, name: string, addedAmount: number, unit: string, category: string }} entry + * @returns {string} new entry id + */ +export function addSessionLogEntry(entry) { + const s = loadShoppingSession(); + const id = newId('sl'); + s.sessionLog.push({ + id, + ingredientId: entry.ingredientId, + productId: entry.productId || undefined, + name: entry.name, + addedAmount: Math.round(entry.addedAmount * 100) / 100, + unit: entry.unit, + category: entry.category, + timestamp: Date.now(), + }); + saveShoppingSession(s); + return id; +} + +/** @param {string} entryId */ +export function removeSessionLogEntry(entryId) { + const s = loadShoppingSession(); + s.sessionLog = s.sessionLog.filter((e) => e.id !== entryId); + saveShoppingSession(s); +} + +export function clearSessionLog() { + const s = loadShoppingSession(); + s.sessionLog = []; + saveShoppingSession(s); +} + /** Kupione ze listy kuchennej → spiżarnia (zawsze ta lista, niezależnie od aktywnej zakładki). */ export function applyCheckedKitchenListToPantry() { const s = loadShoppingState(); diff --git a/js/services/planIngredients.js b/js/services/planIngredients.js index a204d24..18e7ac5 100644 --- a/js/services/planIngredients.js +++ b/js/services/planIngredients.js @@ -218,6 +218,35 @@ export function aggregateRangeIngredientNeed(plans, startDate, numDays) { }); } +/** + * Zapotrzebowanie składników dla konkretnych dni (tablica dateKey-ów). + * @param {Record} plans + * @param {string[]} selectedDays — tablica dateKey-ów ('YYYY-MM-DD') + */ +export function aggregateSelectedDaysIngredientNeed(plans, selectedDays) { + if (!selectedDays || selectedDays.length === 0) return []; + const map = new Map(); + for (const dk of selectedDays) { + const [y, m, d] = dk.split('-').map(Number); + const day = new Date(y, m - 1, d); + const dayPlan = getDayPlan(plans, day); + const lines = flattenDayIngredientLines(dayPlan); + for (const line of lines) { + const key = `${line.ingredientId}\t${line.unit}`; + const cur = map.get(key); + if (!cur) { + map.set(key, { ...line }); + } else { + cur.amount = Math.round((cur.amount + line.amount) * 10) / 10; + } + } + } + return [...map.values()].sort((a, b) => { + const c = a.category.localeCompare(b.category); + return c !== 0 ? c : a.name.localeCompare(b.name, 'pl'); + }); +} + /** * Jedna grupa na porę dnia: nagłówek pory raz, potem bloki przepisów ze składnikami. */ diff --git a/js/storageKeys.js b/js/storageKeys.js index 509ca85..d905a18 100644 --- a/js/storageKeys.js +++ b/js/storageKeys.js @@ -2,3 +2,4 @@ export const PLANS_STORAGE_KEY = 'recipe-planner-plans-v1'; export const PANTRY_STORAGE_KEY = 'recipe-pantry-v1'; export const PANTRY_STORAGE_KEY_V2 = 'recipe-pantry-v2'; export const SHOPPING_STORAGE_KEY = 'recipe-shopping-v1'; +export const SHOPPING_SESSION_KEY = 'recipe-shopping-session-v1'; diff --git a/js/ui/bottomNav.js b/js/ui/bottomNav.js index 016dbea..b680d3e 100644 --- a/js/ui/bottomNav.js +++ b/js/ui/bottomNav.js @@ -46,7 +46,6 @@ export function getBottomNavHTML() { -