-
${escapeHtml(r.title)}
+
${escapeHtml(r.title)}
${r.nutritionPerServing.kcal} kcal
·
@@ -523,43 +582,175 @@ function renderPickerList(slotId) {
`).join('');
}
+function plIngredientWord(n) {
+ if (n === 1) return 'składnik';
+ const m10 = n % 10;
+ const m100 = n % 100;
+ if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return 'składniki';
+ return 'składników';
+}
+
+function updateIngButtons(state) {
+ const btn1 = document.getElementById('planner-ing-add-all');
+ const btn2 = document.getElementById('planner-ing-add-btn');
+
+ const todayCount = (state._todayShortfalls || []).length;
+ const allCount = (state._allForecastShortfalls || []).length;
+
+ if (btn1) {
+ if (todayCount > 0) {
+ btn1.classList.remove('hidden');
+ btn1.disabled = false;
+ btn1.innerHTML = ` Dodaj braki na dziś do listy`;
+ } else {
+ btn1.classList.add('hidden');
+ }
+ }
+ if (btn2) {
+ if (allCount > todayCount) {
+ btn2.classList.remove('hidden');
+ btn2.innerHTML = ` Dodaj braki na cały tydzień`;
+ } else {
+ btn2.classList.add('hidden');
+ }
+ }
+}
+
function renderIngredientsSheet(state) {
const body = document.getElementById('planner-ing-body');
+ const titleEl = document.getElementById('planner-ing-title');
+ const subEl = document.getElementById('planner-ing-sub');
if (!body) return;
- const dayPlan = getDayPlan(state.plans, state.selected);
- const blocks = aggregateDayIngredientsBySlot(dayPlan);
+ const pantry = loadPantry();
+ const forecast = computeFullForecast(state.plans, pantry, state.selected);
- if (blocks.length === 0) {
+ const today = forecast.length > 0 && forecast[0].dayIndex === 0 ? forecast[0] : null;
+ const upcoming = forecast.filter((d) => d.dayIndex > 0 && d.hasShortfall);
+
+ state._todayShortfalls = today ? today.items.filter((it) => !it.enough) : [];
+ state._allForecastShortfalls = [];
+ for (const d of forecast) {
+ for (const it of d.items) {
+ if (!it.enough) state._allForecastShortfalls.push(it);
+ }
+ }
+
+ if (titleEl) {
+ const wd = WEEKDAYS_LONG[state.selected.getDay()];
+ titleEl.textContent = `${wd}, ${state.selected.getDate()} ${MONTHS_SHORT[state.selected.getMonth()]} — składniki`;
+ }
+ if (subEl) subEl.textContent = 'Porównanie potrzeb z zapasami w spiżarni.';
+
+ if (!today || today.items.length === 0) {
body.innerHTML = '
Najpierw zaplanuj posiłki.
';
+ updateIngButtons(state);
return;
}
- body.innerHTML = blocks.map((block) => `
-
-
${escapeHtml(block.mealLabel)}
-
- ${block.recipes.map((rec) => `
-
-
${escapeHtml(rec.recipeTitle)}
-
- ${rec.items.map((ing) => `
- -
-
- ${escapeHtml(ing.name)}
- ${formatAmount(ing.amount)} ${escapeHtml(ing.unit)}
-
- `).join('')}
-
-
- `).join('')}
+ const shortItems = today.items.filter((it) => !it.enough);
+ const okItems = today.items.filter((it) => it.enough);
+ let html = '';
+
+ if (shortItems.length === 0) {
+ html += `
- `).join('');
+
+
Wszystko masz w spiżarni
+
${today.items.length} ${plIngredientWord(today.items.length)} — zapasy wystarczą
+
+
`;
+ } else {
+ html += `
+
+
+
+
+
${shortItems.length} ${plIngredientWord(shortItems.length)} do kupienia
+
Brakuje składników na zaplanowane posiłki
+
+
`;
+ }
+
+ if (shortItems.length > 0) {
+ html += `
+
+ Do kupienia
+
+
+ ${shortItems.map((ing) => `
+ -
+
+
+
${escapeHtml(ing.name)}
+
+ potrzeba ${formatAmount(ing.amount)} ${escapeHtml(ing.pantryUnit)}
+ ·
+ w spiżarni ${ing.pantryQty > 0 ? formatAmount(ing.pantryQty) + ' ' + escapeHtml(ing.pantryUnit) : 'brak'}
+
+
+
+
−${formatAmount(ing.shortfall)}
+
${escapeHtml(ing.pantryUnit)}
+
+ `).join('')}
+
+
`;
+ }
+
+ if (okItems.length > 0) {
+ html += `
+
+ W spiżarni
+
+
+ ${okItems.map((ing) => `
+ -
+
+
+
${escapeHtml(ing.name)}
+
+ potrzeba ${formatAmount(ing.amount)} ${escapeHtml(ing.pantryUnit)}
+ ·
+ masz ${formatAmount(ing.pantryQty)} ${escapeHtml(ing.pantryUnit)}
+
+
+ `).join('')}
+
+
`;
+ }
+
+ if (upcoming.length > 0) {
+ html += `
+
+ Nadchodzące braki
+
+
+ ${upcoming.map((day) => {
+ const wd = WEEKDAYS_LONG[day.date.getDay()];
+ const label = `${wd}, ${day.date.getDate()} ${MONTHS_SHORT[day.date.getMonth()]}`;
+ const shorts = day.items.filter((it) => !it.enough);
+ return `
+
+ ${escapeHtml(label)}
+
+
+ ${shorts.map((it) => `
+ -
+ ${escapeHtml(it.name)}
+ −${formatAmount(it.shortfall)} ${escapeHtml(it.pantryUnit)}
+
`).join('')}
+
+
`;
+ }).join('')}
+
+
`;
+ }
+
+ body.innerHTML = html;
+ updateIngButtons(state);
}
function formatAmount(n) {
@@ -606,11 +797,8 @@ export function setupMealPlanner() {
syncModeToggle(state.mode);
updatePeriodLabel(state.mode, state.weekStart, state.monthAnchor);
syncTodayButton(state.mode, state.weekStart, state.monthAnchor, state.selected);
- if (state.mode === 'week') {
- renderWeekGrid(state.weekStart, state.selected, state.plans);
- } else {
- renderMonthGrid(state.monthAnchor, state.selected, state.plans);
- }
+ renderWeekGrid(state.weekStart, state.selected, state.plans);
+ renderMonthGrid(state.monthAnchor, state.selected, state.plans);
renderDayContent(state);
};
@@ -622,20 +810,6 @@ export function setupMealPlanner() {
bindDayClicks(weekGrid?.parentElement, state, rerender);
bindDayClicks(monthGrid?.parentElement, state, rerender);
- document.getElementById('planner-cal-mode')?.addEventListener('click', (e) => {
- const btn = e.target.closest('[data-cal-mode]');
- if (!btn) return;
- const mode = btn.getAttribute('data-cal-mode');
- if (mode !== 'week' && mode !== 'month') return;
- state.mode = mode;
- if (mode === 'week') {
- state.weekStart = startOfWeekMonday(state.selected);
- } else {
- state.monthAnchor = startOfMonth(state.selected);
- }
- rerender();
- });
-
document.getElementById('cal-prev')?.addEventListener('click', () => {
if (state.mode === 'week') {
state.weekStart = addWeeks(state.weekStart, -1);
@@ -766,59 +940,56 @@ export function setupMealPlanner() {
});
document.getElementById('planner-ing-add-all')?.addEventListener('click', () => {
- const body = document.getElementById('planner-ing-body');
- const rows = body?.querySelectorAll('.planner-ing-row');
- const n = rows?.length ?? 0;
- if (n === 0) return;
- const lines = [];
- rows.forEach((row) => {
- const id = row.getAttribute('data-ingredient-id');
- const amount = parseFloat(row.getAttribute('data-amount') || '');
- const unit = row.getAttribute('data-unit') || '';
- const category = row.getAttribute('data-category') || '';
- if (!id || !Number.isFinite(amount)) return;
- lines.push({
- ingredientId: id,
- amount,
- unit,
- category,
- sourceNote: 'Z planu dnia',
- });
- });
+ const items = state._todayShortfalls || [];
+ if (items.length === 0) return;
+ const lines = items.map((it) => ({
+ ingredientId: it.ingredientId,
+ amount: it.shortfall,
+ unit: it.pantryUnit,
+ category: it.category,
+ sourceNote: 'Braki z planu dnia',
+ }));
addOrMergeShoppingLines(lines);
- showPlannerToast(`Dodano ${lines.length} składników na listę.`);
+ showPlannerToast(`Dodano ${lines.length} braków na listę zakupów.`);
window.refreshShopping?.();
closeSheet(ingBackdrop, ingSheet);
});
document.getElementById('planner-ing-add-btn')?.addEventListener('click', () => {
- const body = document.getElementById('planner-ing-body');
- const selected = body?.querySelectorAll('.planner-ing-row.ingredient-active');
- const n = selected?.length ?? 0;
- if (n === 0) {
- showPlannerToast('Zaznacz składniki na liście albo użyj „Dodaj wszystkie”.');
- return;
+ const items = state._allForecastShortfalls || [];
+ if (items.length === 0) return;
+ const map = new Map();
+ for (const it of items) {
+ const key = it.ingredientId;
+ if (map.has(key)) {
+ const cur = map.get(key);
+ cur.amount = Math.round((cur.amount + it.shortfall) * 10) / 10;
+ } else {
+ map.set(key, {
+ ingredientId: it.ingredientId,
+ amount: it.shortfall,
+ unit: it.pantryUnit,
+ category: it.category,
+ sourceNote: 'Braki z planu tygodnia',
+ });
+ }
}
- const lines = [];
- selected.forEach((row) => {
- const id = row.getAttribute('data-ingredient-id');
- const amount = parseFloat(row.getAttribute('data-amount') || '');
- const unit = row.getAttribute('data-unit') || '';
- const category = row.getAttribute('data-category') || '';
- if (!id || !Number.isFinite(amount)) return;
- lines.push({
- ingredientId: id,
- amount,
- unit,
- category,
- sourceNote: 'Z planu dnia',
- });
- });
+ const lines = [...map.values()];
addOrMergeShoppingLines(lines);
- showPlannerToast(`Dodano ${lines.length} pozycji na listę.`);
+ showPlannerToast(`Dodano ${lines.length} braków na listę zakupów.`);
window.refreshShopping?.();
closeSheet(ingBackdrop, ingSheet);
});
rerender();
+
+ bindCalendarSwipeGesture(state, rerender);
+
+ requestAnimationFrame(() => {
+ const ww = document.getElementById('calendar-week-wrap');
+ const mw = document.getElementById('calendar-month-wrap');
+ const t = 'max-height 300ms ease, opacity 200ms ease';
+ if (ww) ww.style.transition = t;
+ if (mw) mw.style.transition = t;
+ });
}
diff --git a/stacks/recipe/js/views/Pantry.js b/stacks/recipe/js/views/Pantry.js
index 27a8da6..62f2085 100644
--- a/stacks/recipe/js/views/Pantry.js
+++ b/stacks/recipe/js/views/Pantry.js
@@ -55,10 +55,10 @@ export function getPantryHTML() {
-
+
@@ -142,9 +142,9 @@ function renderCategoryChips() {
const active = selectedCategories.has(k);
const icon = CATEGORY_ICONS[k] || 'fa-jar';
const cls = active
- ? 'shrink-0 inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-[11px] font-semibold bg-gray-900 text-white transition-colors'
- : 'shrink-0 inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-[11px] font-semibold bg-gray-100 text-gray-600 hover:bg-gray-200 transition-colors';
- return `
`;
+ ? 'shrink-0 inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-xs font-semibold bg-gray-900 text-white transition-colors'
+ : 'shrink-0 inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full text-xs font-semibold bg-gray-100 text-gray-600 hover:bg-gray-200 transition-colors';
+ return `
`;
}).join('');
wrap.querySelectorAll('.pv2-cat-chip').forEach(btn => {
@@ -176,15 +176,15 @@ function chipHtml(id, pantry) {
const u = unitLabel(def.pantryUnit);
if (qty > 0) {
- return `