diff --git a/src/lib/server/nutritionMatcher.ts b/src/lib/server/nutritionMatcher.ts index c25a308..674c77e 100644 --- a/src/lib/server/nutritionMatcher.ts +++ b/src/lib/server/nutritionMatcher.ts @@ -706,6 +706,28 @@ export async function generateNutritionMappings( const itemDe = sectionDe.list[itemIdx]; const itemEn = sectionEn?.list?.[itemIdx]; + // Anchor-tag references to other recipes — their nutrition + // is resolved separately via resolveReferencedNutrition() + const refSlug = parseAnchorRecipeRef(itemDe.name || ''); + if (refSlug) { + mappings.push({ + sectionIndex: sectionIdx, + ingredientIndex: itemIdx, + ingredientName: itemEn?.name || itemDe.name, + ingredientNameDe: itemDe.name, + matchMethod: 'none', + matchConfidence: 0, + gramsPerUnit: 0, + defaultAmountUsed: false, + unitConversionSource: 'none', + manuallyEdited: false, + excluded: true, + recipeRef: refSlug, + recipeRefMultiplier: 1, + }); + continue; + } + const mapping = await matchIngredient( itemDe.name, itemEn?.name || undefined, @@ -838,14 +860,25 @@ export type ReferencedNutritionResult = { * Build nutrition totals for referenced recipes: * 1. Base recipe references (type='reference' with populated baseRecipeRef) * 2. Anchor-tag references in ingredient names () + * + * When nutritionMappings are provided, uses recipeRefMultiplier from the + * mapping for anchor-tag refs (allowing user-configured fractions). */ export async function resolveReferencedNutrition( ingredients: any[], + nutritionMappings?: any[], ): Promise { const { Recipe } = await import('$models/Recipe'); const results: ReferencedNutritionResult[] = []; const processedSlugs = new Set(); + // Build mapping index for recipeRefMultiplier lookup + const mappingIndex = new Map( + (nutritionMappings || []) + .filter((m: any) => m.recipeRef) + .map((m: any) => [m.recipeRef, m]) + ); + for (const section of ingredients) { // Type 1: Base recipe references if (section.type === 'reference' && section.baseRecipeRef) { @@ -873,10 +906,11 @@ export async function resolveReferencedNutrition( .lean(); if (!refRecipe?.nutritionMappings?.length) continue; + const mult = mappingIndex.get(refSlug)?.recipeRefMultiplier ?? 1; const nutrition = computeRecipeNutritionTotals( refRecipe.ingredients || [], refRecipe.nutritionMappings, 1 ); - results.push({ shortName: refSlug, name: refRecipe.name, nutrition, baseMultiplier: 1 }); + results.push({ shortName: refSlug, name: refRecipe.name, nutrition, baseMultiplier: mult }); } } } diff --git a/src/models/Recipe.ts b/src/models/Recipe.ts index 5a99071..42ba182 100644 --- a/src/models/Recipe.ts +++ b/src/models/Recipe.ts @@ -180,6 +180,8 @@ const RecipeSchema = new mongoose.Schema( unitConversionSource: { type: String, enum: ['direct', 'density', 'usda_portion', 'estimate', 'manual', 'none'] }, manuallyEdited: { type: Boolean, default: false }, excluded: { type: Boolean, default: false }, + recipeRef: { type: String }, + recipeRefMultiplier: { type: Number, default: 1 }, }], // Translation metadata for tracking changes diff --git a/src/routes/[recipeLang=recipeLang]/edit/[name]/+page.svelte b/src/routes/[recipeLang=recipeLang]/edit/[name]/+page.svelte index 47da1fd..9c092ce 100644 --- a/src/routes/[recipeLang=recipeLang]/edit/[name]/+page.svelte +++ b/src/routes/[recipeLang=recipeLang]/edit/[name]/+page.svelte @@ -314,7 +314,7 @@ }); await loadGlobalOverwrites(); initGlobalToggles(); - const mapped = nutritionMappings.filter((m: any) => m.matchMethod !== 'none').length; + const mapped = nutritionMappings.filter((m: any) => m.matchMethod !== 'none' || m.recipeRef).length; toast.success(`Nährwerte generiert: ${mapped}/${result.count} Zutaten zugeordnet`); } catch (e: any) { toast.error(`Fehler: ${e.message}`); @@ -675,6 +675,29 @@ .manual-row { border-left: 2px solid var(--nord13); } + .recipe-ref-row { + border-left: 2px solid var(--nord8); + } + .source-badge.recipe-ref { + background: var(--nord8); + color: var(--nord0); + } + .recipe-ref-label { + font-size: 0.85rem; + color: var(--nord8); + font-weight: 600; + } + .ref-multiplier { + display: inline-flex; + align-items: center; + gap: 0.3rem; + font-size: 0.8rem; + color: var(--color-text-secondary); + margin-left: 0.5rem; + } + .ref-multiplier .gpu-input { + width: 3.5rem; + } .excluded-label { font-style: italic; color: var(--nord11); @@ -978,14 +1001,14 @@

- {nutritionMappings.filter((m) => m.matchMethod !== 'none').length}/{nutritionMappings.length} Zutaten zugeordnet + {nutritionMappings.filter((m) => m.matchMethod !== 'none' || m.recipeRef).length}/{nutritionMappings.length} Zutaten zugeordnet

{#each nutritionMappings as m, i (mappingKey(m))} {@const key = mappingKey(m)} - + - + {/each} diff --git a/src/routes/api/[recipeLang=recipeLang]/items/[name]/+server.ts b/src/routes/api/[recipeLang=recipeLang]/items/[name]/+server.ts index b6ad046..c948ae3 100644 --- a/src/routes/api/[recipeLang=recipeLang]/items/[name]/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/items/[name]/+server.ts @@ -157,7 +157,7 @@ export const GET: RequestHandler = async ({ params }) => { } // Resolve nutrition from referenced recipes (base refs + anchor tags) - recipe.referencedNutrition = await resolveReferencedNutrition(rawRecipe.ingredients || []); + recipe.referencedNutrition = await resolveReferencedNutrition(rawRecipe.ingredients || [], rawRecipe.nutritionMappings); // Merge English alt/caption with original image paths const imagesArray = Array.isArray(rawRecipe.images) ? rawRecipe.images : (rawRecipe.images ? [rawRecipe.images] : []); @@ -184,6 +184,6 @@ export const GET: RequestHandler = async ({ params }) => { recipe.instructions = mapBaseRecipeRefs(recipe.instructions); } // Resolve nutrition from referenced recipes (base refs + anchor tags) - recipe.referencedNutrition = await resolveReferencedNutrition(rawRecipe.ingredients || []); + recipe.referencedNutrition = await resolveReferencedNutrition(rawRecipe.ingredients || [], rawRecipe.nutritionMappings); return json(recipe); }; diff --git a/src/routes/api/[recipeLang=recipeLang]/json-ld/[name]/+server.ts b/src/routes/api/[recipeLang=recipeLang]/json-ld/[name]/+server.ts index ceef0f7..3d82edd 100644 --- a/src/routes/api/[recipeLang=recipeLang]/json-ld/[name]/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/json-ld/[name]/+server.ts @@ -17,7 +17,7 @@ export const GET: RequestHandler = async ({ params, setHeaders }) => { throw error(404, "Recipe not found"); } - const referencedNutrition = await resolveReferencedNutrition(recipe.ingredients || []); + const referencedNutrition = await resolveReferencedNutrition(recipe.ingredients || [], recipe.nutritionMappings); const jsonLd = generateRecipeJsonLd(recipe, referencedNutrition); // Set appropriate headers for JSON-LD diff --git a/src/types/types.ts b/src/types/types.ts index 4eebf47..7c8e5d8 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -33,6 +33,8 @@ export type NutritionMapping = { manuallyEdited: boolean; excluded: boolean; per100g?: NutritionPer100g; + recipeRef?: string; + recipeRefMultiplier?: number; }; // Translation status enum
#ZutatQuelleTreffer / SucheKonf.g/u
{i + 1} {m.ingredientNameDe || m.ingredientName} @@ -994,7 +1017,9 @@ {/if} - {#if m.excluded} + {#if m.recipeRef} + REF + {:else if m.excluded} SKIP {:else if m.matchMethod !== 'none'} {(m.source || 'usda').toUpperCase()} @@ -1005,61 +1030,73 @@
- {#if m.excluded} + {#if m.recipeRef} + {m.recipeRef} + + {:else if m.excluded} Übersprungen {:else if m.matchMethod !== 'none' && !searchQueries[key]} {m.nutritionDbName || '—'} {/if} - handleSearchInput(key, e.currentTarget.value)} - /> - {#if searchResults[key]?.length > 0} -
    - {#each searchResults[key] as result (result.id)} -
  • - -
  • - {/each} -
- {/if} -
- { globalToggle[key] = !globalToggle[key]; }} /> - {#if m.manuallyEdited || m.excluded} - + {#if !m.recipeRef} + handleSearchInput(key, e.currentTarget.value)} + /> + {#if searchResults[key]?.length > 0} +
    + {#each searchResults[key] as result (result.id)} +
  • + +
  • + {/each} +
{/if} -
+
+ { globalToggle[key] = !globalToggle[key]; }} /> + {#if m.manuallyEdited || m.excluded} + + {/if} +
+ {/if}
{m.matchConfidence ? (m.matchConfidence * 100).toFixed(0) + '%' : '—'}{m.recipeRef ? '—' : (m.matchConfidence ? (m.matchConfidence * 100).toFixed(0) + '%' : '—')} - {#if m.manuallyEdited} + {#if m.recipeRef} + — + {:else if m.manuallyEdited} {:else} {m.gramsPerUnit || '—'} {/if} - + {#if !m.recipeRef} + + {/if}