Add ingredients' products
Some checks failed
Build and Deploy / build-and-push (push) Failing after 1m20s

This commit is contained in:
2026-04-07 22:51:30 +02:00
parent ac32e05c31
commit 868862d031
11 changed files with 576 additions and 57 deletions

View File

@@ -1,4 +1,4 @@
import { INGREDIENTS, RECIPES } from '../data/catalog.js?v=2';
import { INGREDIENTS, RECIPES, PRODUCTS, getProductsForIngredient } from '../data/catalog.js?v=6';
import { MEAL_SLOTS } from '../planner/mealSlots.js';
import {
addDays,
@@ -13,8 +13,9 @@ import {
loadPlans,
newPlanEntryId,
savePlans,
} from '../services/planStore.js';
import { dayHasAnyMeal } from '../services/planIngredients.js';
} from '../services/planStore.js?v=2';
import { dayHasAnyMeal, autoSelectProducts, saveLastProductSelection } from '../services/planIngredients.js?v=3';
import { loadPantry } from '../services/pantryShopping.js?v=2';
import { showAppToast } from './toast.js';
import {
bindCalendarDayClicks,
@@ -113,21 +114,26 @@ export function setupMealPlanEditor() {
altOpen: new Set(),
addOpen: false,
addQuery: '',
productSelections: {},
};
/* ── helpers ───────────────────────────────────── */
function nutFor(ingredientId, amount, unit) {
const def = INGREDIENTS[ingredientId];
if (!def?.nutritionPer100g) return null;
if (!def) return null;
const productId = S.productSelections[ingredientId] || null;
const product = productId ? PRODUCTS[productId] : null;
const nutrition = product?.nutritionPer100g || def.nutritionPer100g;
if (!nutrition) return null;
let g = amount;
if ((unit === 'szt.' || unit === 'szt') && def.weightPerPiece) g = amount * def.weightPerPiece;
const f = g / 100;
return {
kcal: Math.round(def.nutritionPer100g.kcal * f),
protein: Math.round(def.nutritionPer100g.protein * f * 10) / 10,
fat: Math.round(def.nutritionPer100g.fat * f * 10) / 10,
carbs: Math.round(def.nutritionPer100g.carbs * f * 10) / 10,
kcal: Math.round(nutrition.kcal * f),
protein: Math.round(nutrition.protein * f * 10) / 10,
fat: Math.round(nutrition.fat * f * 10) / 10,
carbs: Math.round(nutrition.carbs * f * 10) / 10,
};
}
@@ -291,8 +297,14 @@ export function setupMealPlanEditor() {
const modDot = modified ? '<span class="w-1.5 h-1.5 rounded-full bg-amber-400 shrink-0"></span>' : '';
html += `<div class="mpe-ing-row rounded-xl p-2.5" style="${rowStyle}" data-orig-id="${esc(id)}" data-type="recipe">`;
const selectedProductId = S.productSelections[eid];
const selectedProduct = selectedProductId ? PRODUCTS[selectedProductId] : null;
const productBadge = selectedProduct
? `<div class="flex items-center gap-1 mt-0.5"><span class="text-[10px] text-emerald-400 truncate">${esc(selectedProduct.name)}</span><button type="button" class="mpe-change-product text-[9px] text-gray-500 hover:text-gray-300 transition-colors" data-eid="${esc(eid)}" data-orig-id="${esc(id)}">zmień</button></div>`
: '';
html += `<div class="flex items-center gap-2">`;
html += `<div class="flex-1 min-w-0"><span class="text-[12px] font-semibold text-gray-900 truncate block">${esc(eName)}</span></div>`;
html += `<div class="flex-1 min-w-0"><span class="text-[12px] font-semibold text-gray-900 truncate block">${esc(eName)}</span>${productBadge}</div>`;
html += `<div class="shrink-0 flex items-center gap-2">`;
html += shuffleBtn;
html += `<button type="button" class="mpe-edit-amt shrink-0 flex items-center gap-1 px-2 py-1 rounded-lg hover:bg-gray-100 transition-colors" data-orig-id="${esc(id)}" data-type="recipe">`;
@@ -446,6 +458,13 @@ export function setupMealPlanEditor() {
S.addOpen = false;
S.addQuery = '';
// Auto-select products from pantry, then apply saved selections
const pantry = loadPantry();
S.productSelections = autoSelectProducts(recipe, pantry);
if (opts.entry?.productSelections) {
Object.assign(S.productSelections, opts.entry.productSelections);
}
if (opts.date && opts.slotId) {
S.date = startOfDay(new Date(opts.date));
S.calDate = new Date(S.date);
@@ -496,6 +515,7 @@ export function setupMealPlanEditor() {
if (Object.keys(ov).length > 0) entry.amountOverrides = ov;
}
if (S.added.length > 0) entry.addedIngredients = S.added.map((a) => ({ ingredientId: a.ingredientId, amount: a.amount, unit: a.unit }));
if (Object.keys(S.productSelections).length > 0) entry.productSelections = { ...S.productSelections };
return entry;
}
@@ -650,12 +670,76 @@ export function setupMealPlanEditor() {
if (altPick) {
const origId = altPick.dataset.origId;
const altId = altPick.dataset.altId;
const prevEid = S.subs[origId] || origId;
if (altId === origId) delete S.subs[origId]; else S.subs[origId] = altId;
const newEid = S.subs[origId] || origId;
// Update product selection for the new effective ingredient
if (newEid !== prevEid) {
delete S.productSelections[prevEid];
const newProducts = getProductsForIngredient(newEid);
if (newProducts.length > 0) {
const pantry = loadPantry();
const auto = autoSelectProducts({ ingredients: [{ ingredientId: newEid }] }, pantry);
if (auto[newEid]) S.productSelections[newEid] = auto[newEid];
}
}
S.altOpen.delete(origId);
renderIngList(); renderNutrition();
return;
}
const changeProd = e.target.closest('.mpe-change-product');
if (changeProd) {
const eid = changeProd.dataset.eid;
const products = getProductsForIngredient(eid);
if (products.length === 0) return;
// Toggle product picker open/closed using a data attribute on the row
const row = changeProd.closest('.mpe-ing-row');
const existing = row?.querySelector('.mpe-product-picker');
if (existing) { existing.remove(); return; }
const currentPid = S.productSelections[eid] || null;
const origId = changeProd.dataset.origId || eid;
const recipe = RECIPES[S.recipeId];
const recipeIng = recipe?.ingredients.find(i => i.ingredientId === origId);
const ingUnit = recipeIng?.unit || 'g';
const ingBase = S.overrides[origId] ?? recipeIng?.amount ?? 0;
const ingDisp = ingBase * S.servings;
const checkmark = (sel) => `<span class="ml-auto self-center w-[18px] h-[18px] rounded-full shrink-0 flex items-center justify-center" style="border:1.5px solid #56534f; background:transparent;">${sel ? '<i class="fas fa-check" style="color:#9b978f; font-size:8px; line-height:1; display:block; transform:translateY(0.5px);"></i>' : ''}</span>`;
let pickerHtml = '<div class="mpe-product-picker mt-2 ml-1 space-y-1">';
for (const p of products) {
const isSel = currentPid === p.id;
const pNut = p.nutritionPer100g;
// Calculate nutrition for the actual ingredient amount using product values
const def = INGREDIENTS[eid];
let g = ingDisp;
if ((ingUnit === 'szt.' || ingUnit === 'szt') && def?.weightPerPiece) g = ingDisp * def.weightPerPiece;
const f = g / 100;
const n = pNut ? { kcal: Math.round(pNut.kcal * f), protein: Math.round(pNut.protein * f * 10) / 10, fat: Math.round(pNut.fat * f * 10) / 10, carbs: Math.round(pNut.carbs * f * 10) / 10 } : null;
const nLine = n ? `<div class="text-[10px] text-gray-400 mt-0.5 tabular-nums">${n.kcal} kcal · ${n.protein}g B · ${n.fat}g T · ${n.carbs}g W</div>` : '';
pickerHtml += `<button type="button" class="mpe-prod-pick w-full text-left p-2.5 rounded-lg transition-all" style="background:#2f2f2d !important; background-image:none !important; border:none !important; box-shadow:none !important;" data-eid="${esc(eid)}" data-prod-id="${esc(p.id)}">
<div class="flex items-center gap-3"><div class="min-w-0 flex-1"><div class="text-[11px] font-semibold text-gray-900">${esc(p.name)}</div>${nLine}</div>${checkmark(isSel)}</div>
</button>`;
}
pickerHtml += '</div>';
row?.insertAdjacentHTML('beforeend', pickerHtml);
return;
}
const prodPick = e.target.closest('.mpe-prod-pick');
if (prodPick) {
const eid = prodPick.dataset.eid;
const prodId = prodPick.dataset.prodId;
if (prodId) {
S.productSelections[eid] = prodId;
saveLastProductSelection(eid, prodId);
}
renderIngList(); renderNutrition();
return;
}
const editAmt = e.target.closest('.mpe-edit-amt');
if (editAmt && !editAmt.disabled) {
startAmountEdit(editAmt);