-
+
${recipe.image
? `
})
`
@@ -1258,14 +1258,6 @@ export function setupMealPlanner() {
}
return;
}
- const openRecipe = e.target.closest('.planner-open-recipe');
- if (openRecipe) {
- const recipeId = openRecipe.getAttribute('data-recipe-id');
- if (recipeId && window.openRecipeDetail) {
- window.openRecipeDetail(recipeId);
- }
- return;
- }
const addBtn = e.target.closest('.planner-add-meal');
if (addBtn) {
const slotId = addBtn.getAttribute('data-slot-id');
@@ -1299,6 +1291,19 @@ export function setupMealPlanner() {
});
return;
}
+ const openRecipe = e.target.closest('.planner-open-recipe');
+ if (openRecipe) {
+ const recipeId = openRecipe.getAttribute('data-recipe-id');
+ if (recipeId && window.openRecipeDetail) {
+ const slotId = openRecipe.closest('[data-slot-id]')?.getAttribute('data-slot-id');
+ const entryId = openRecipe.closest('[data-entry-id]')?.getAttribute('data-entry-id');
+ const key = dateKey(state.selected);
+ const entries = slotId ? state.plans[key]?.[slotId] : null;
+ const entry = Array.isArray(entries) ? entries.find((item) => item && item.id === entryId) : null;
+ window.openRecipeDetail(recipeId, entry ? { plannedEntry: entry } : {});
+ }
+ return;
+ }
});
const closePicker = () => {
diff --git a/js/views/RecipeDetailV2.js b/js/views/RecipeDetailV2.js
index 4eb71e7..03dc89f 100644
--- a/js/views/RecipeDetailV2.js
+++ b/js/views/RecipeDetailV2.js
@@ -1,5 +1,6 @@
-import { RECIPES, INGREDIENTS } from '../data/catalog.js?v=8';
+import { RECIPES, INGREDIENTS, PRODUCTS } from '../data/catalog.js?v=8';
import { MEAL_SLOTS } from '../planner/mealSlots.js';
+import { createIngredientCardController, getIngredientCardHTML } from '../ui/ingredientCard.js?v=20260410-107';
function escapeHtml(s) {
return String(s)
@@ -99,30 +100,117 @@ export function getRecipeDetailHTML() {
+ ${getIngredientCardHTML({ idBase: 'rd-ing-card' })}
`;
}
/* ── state ─────────────────────────────────────────────── */
let currentRecipeId = null;
+let currentMode = 'catalog';
let currentServings = 1;
let currentSubstitutions = {};
+let currentExcludedIngredients = new Set();
+let currentAmountOverrides = {};
+let currentAddedIngredients = [];
+let currentProductSelections = {};
let expandedAlternatives = new Set();
+let ingredientCard = null;
+
+function isPlannedMode() {
+ return currentMode === 'planned';
+}
function getEffectiveIngredientId(originalId) {
return currentSubstitutions[originalId] || originalId;
}
+function clampServings(value) {
+ return Math.max(1, Math.min(12, Number(value) || 1));
+}
+
+function cloneAddedIngredients(items) {
+ if (!Array.isArray(items)) return [];
+ return items
+ .filter((item) => item && typeof item.ingredientId === 'string' && typeof item.amount === 'number' && typeof item.unit === 'string')
+ .map((item) => ({ ingredientId: item.ingredientId, amount: item.amount, unit: item.unit }));
+}
+
+function getSelectedProduct(productId) {
+ return productId && PRODUCTS[productId] ? PRODUCTS[productId] : null;
+}
+
+function getProductSelectionForIngredient(...ingredientIds) {
+ for (const ingredientId of ingredientIds) {
+ if (!ingredientId) continue;
+ const productId = currentProductSelections[ingredientId];
+ if (getSelectedProduct(productId)) return productId;
+ }
+ return null;
+}
+
+function buildVisibleIngredients(recipe) {
+ if (!recipe) return [];
+
+ const rows = [];
+
+ for (const ing of recipe.ingredients) {
+ const originalId = ing.ingredientId;
+ if (currentExcludedIngredients.has(originalId)) continue;
+
+ const effectiveId = getEffectiveIngredientId(originalId);
+ const effectiveDef = INGREDIENTS[effectiveId];
+ const productId = getProductSelectionForIngredient(effectiveId, originalId);
+ rows.push({
+ key: `recipe:${originalId}`,
+ originalId,
+ ingredientId: effectiveId,
+ name: effectiveDef?.name || effectiveId,
+ amount: (currentAmountOverrides[originalId] ?? ing.amount) * currentServings,
+ unit: ing.unit,
+ productId,
+ productName: getSelectedProduct(productId)?.name || '',
+ added: false,
+ alternatives: Array.isArray(ing.alternatives) ? ing.alternatives : [],
+ });
+ }
+
+ currentAddedIngredients.forEach((item, index) => {
+ const def = INGREDIENTS[item.ingredientId];
+ const productId = getProductSelectionForIngredient(item.ingredientId);
+ rows.push({
+ key: `added:${index}:${item.ingredientId}`,
+ originalId: item.ingredientId,
+ ingredientId: item.ingredientId,
+ name: def?.name || item.ingredientId,
+ amount: item.amount * currentServings,
+ unit: item.unit,
+ productId,
+ productName: getSelectedProduct(productId)?.name || '',
+ added: true,
+ alternatives: [],
+ });
+ });
+
+ return rows;
+}
+
/* ── populate ──────────────────────────────────────────── */
-function populateDetail(recipeId) {
+function populateDetail(recipeId, options = {}) {
const recipe = RECIPES[recipeId];
if (!recipe) return;
currentRecipeId = recipeId;
- currentServings = 1;
- currentSubstitutions = {};
+ currentMode = options.plannedEntry ? 'planned' : 'catalog';
+ currentServings = clampServings(options.servings ?? options.plannedEntry?.servings ?? 1);
+ currentSubstitutions = { ...(options.substitutions || options.plannedEntry?.substitutions || {}) };
+ currentExcludedIngredients = new Set(options.excludedIngredients || options.plannedEntry?.excludedIngredients || []);
+ currentAmountOverrides = { ...(options.amountOverrides || options.plannedEntry?.amountOverrides || {}) };
+ currentAddedIngredients = cloneAddedIngredients(options.addedIngredients || options.plannedEntry?.addedIngredients);
+ currentProductSelections = { ...(options.productSelections || options.plannedEntry?.productSelections || {}) };
expandedAlternatives.clear();
+ ingredientCard?.close();
const heroImg = document.getElementById('rd-hero-img');
const heroLabel = document.getElementById('rd-hero-label');
@@ -138,6 +226,7 @@ function populateDetail(recipeId) {
}
document.getElementById('rd-title').textContent = recipe.title;
document.getElementById('rd-time').textContent = `${recipe.minutes} min`;
+ document.getElementById('rd-add-to-planner-btn')?.classList.toggle('hidden', isPlannedMode());
const tagsHtml = [];
for (const slotId of recipe.allowedSlots) {
@@ -165,19 +254,22 @@ function populateDetail(recipeId) {
/* ── helpers ───────────────────────────────────────────── */
-function nutritionForAmount(ingredientId, amount, unit) {
+function nutritionForAmount(ingredientId, amount, unit, productIdOverride = null) {
const def = INGREDIENTS[ingredientId];
- if (!def?.nutritionPer100g) return null;
+ const productId = productIdOverride || getProductSelectionForIngredient(ingredientId);
+ const product = getSelectedProduct(productId);
+ const nutrition = product?.nutritionPer100g || def?.nutritionPer100g;
+ if (!def || !nutrition) return null;
let grams = amount;
if ((unit === 'szt.' || unit === 'szt') && def.weightPerPiece) {
grams = amount * def.weightPerPiece;
}
const f = grams / 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,
};
}
@@ -189,9 +281,8 @@ function fmtAmt(n) {
function computeEffectiveNutritionTotals(recipe) {
let kcal = 0, protein = 0, fat = 0, carbs = 0;
- for (const ing of recipe.ingredients) {
- const effectiveId = getEffectiveIngredientId(ing.ingredientId);
- const n = nutritionForAmount(effectiveId, ing.amount * currentServings, ing.unit);
+ for (const ing of buildVisibleIngredients(recipe)) {
+ const n = nutritionForAmount(ing.ingredientId, ing.amount, ing.unit, ing.productId);
if (n) {
kcal += n.kcal;
protein += n.protein;
@@ -209,6 +300,25 @@ function computeEffectiveNutritionTotals(recipe) {
function renderNutritionSummary(recipe) {
const total = computeEffectiveNutritionTotals(recipe);
+ const servingsHtml = isPlannedMode()
+ ? `
+
+
Porcje
+
${currentServings}
+
`
+ : `
+
+
Porcje
+
+
+ ${currentServings}
+
+
+
`;
return `
@@ -235,18 +345,7 @@ function renderNutritionSummary(recipe) {