Lekkere, koolhydraatarme snacks binnen jouw gekozen bereik
+
+
+
+ Koolhydraten per portie
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Belangrijk: De getoonde voedingswaarden zijn gebaseerd op de NEVO-database (RIVM) en kunnen afwijken van merkproducten. Controleer altijd het etiket van jouw eigen producten. Deze suggesties zijn geen medisch advies en vervangen geen begeleiding van een diΓ«tist of arts.
+
+
+
@@ -2666,7 +3011,312 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
const CATEGORIES = ['Brood', 'Zuivel', 'Fruit', 'Groente', 'Vlees/Vis', 'Dranken', 'Tussendoortjes', 'Snoep/Koek', 'Overig'];
- // Map NEVO categories to app categories for filtering
+ // ===== Tussendoortjes Recepten =====
+ const TUSSENDOORTJES_RECIPES = [
+ {
+ naam: 'Chocolade-Frambozenbowl',
+ bereiding: 'Schep de kwark in een kom en meng de cacaopoeder er glad door. Was de frambozen en verdeel ze over de kwark. Rasp de pure chocolade erover en serveer direct.',
+ ingredienten: [
+ { n: '305', naam: 'Kwark magere', portie: 175 },
+ { n: '161', naam: 'Frambozen', portie: 50 },
+ { n: '430', naam: 'Cacaopoeder', portie: 8 },
+ { n: '432', naam: 'Pure chocolade', portie: 15 },
+ ],
+ },
+ {
+ naam: 'Aardbei-Chocoladekwark',
+ bereiding: 'Was de aardbeien en snijd ze in plakjes. Meng de kwark met de cacaopoeder. Schep de aardbeien erdoor. Rasp de pure chocolade erover en geniet.',
+ ingredienten: [
+ { n: '305', naam: 'Kwark magere', portie: 150 },
+ { n: '148', naam: 'Aardbeien', portie: 100 },
+ { n: '432', naam: 'Pure chocolade', portie: 18 },
+ ],
+ },
+ {
+ naam: 'Warme Appel-Kaneelkwark',
+ bereiding: 'Schil de appel, verwijder het klokhuis en snijd in kleine blokjes. Doe in een pannetje met een scheutje water en de kaneel. Verwarm 3 minuten op middelhoog vuur. Schep de warme appel door de kwark en serveer lauwwarm.',
+ ingredienten: [
+ { n: '305', naam: 'Kwark magere', portie: 180 },
+ { n: '147', naam: 'Appel', portie: 100 },
+ { n: '826', naam: 'Kaneel', portie: 2 },
+ ],
+ },
+ ];
+
+ // ===== Inspiratie (Vind inspiratie) state =====
+ let _inspiratieMoment = null; // eetmoment id: 'Tussendoor 1/2/3'
+ let _inspiratieDate = null; // date string from diary
+ let _lastSuggestieIds = []; // track last set of names to avoid immediate repeats
+ let _cachedGeneratedSet = null; // current generated set (array of generated recipe objects)
+
+ // ===== Inspiratie ingredient pool (NEVO-based) =====
+ const INSPIRATIE_POOL = {
+ base: [
+ { n: '305', naam: 'Kwark magere', kh_per100: 3.8, eiwit_per100: 8.4, minPortie: 125, maxPortie: 200 },
+ { n: '306', naam: 'Kwark halfvolle', kh_per100: 4.0, eiwit_per100: 7.5, minPortie: 125, maxPortie: 200 },
+ { n: '654', naam: 'Cottage cheese', kh_per100: 2.3, eiwit_per100: 11.2, minPortie: 125, maxPortie: 200 },
+ { n: '2503', naam: 'Griekse yoghurt volle', kh_per100: 3.8, eiwit_per100: 3.8, minPortie: 125, maxPortie: 175 },
+ ],
+ fruit: [
+ { n: '161', naam: 'Frambozen', kh_per100: 4.5, minPortie: 40, maxPortie: 75 },
+ { n: '148', naam: 'Aardbeien', kh_per100: 5.1, minPortie: 50, maxPortie: 100 },
+ { n: '152', naam: 'Blauwe bessen', kh_per100: 11, minPortie: 30, maxPortie: 60 },
+ { n: '155', naam: 'Bosbessen', kh_per100: 6, minPortie: 40, maxPortie: 75 },
+ { n: '157', naam: 'Bramen', kh_per100: 5.1, minPortie: 40, maxPortie: 75 },
+ { n: '147', naam: 'Appel', kh_per100: 12, minPortie: 50, maxPortie: 100 },
+ ],
+ topping: [
+ { n: '198', naam: 'Amandelen', kh_per100: 7.1, minPortie: 8, maxPortie: 15 },
+ { n: '206', naam: 'Walnoten', kh_per100: 5.1, minPortie: 8, maxPortie: 15 },
+ { n: '1895', naam: 'Pecannoten', kh_per100: 4.4, minPortie: 8, maxPortie: 15 },
+ { n: '5497', naam: 'Kokos geraspt', kh_per100: 8.6, minPortie: 5, maxPortie: 12 },
+ { n: '3447', naam: 'Chiazaad', kh_per100: 7.7, minPortie: 5, maxPortie: 10 },
+ ],
+ flavor: [
+ { n: '430', naam: 'Cacaopoeder', kh_per100: 10.5, minPortie: 5, maxPortie: 10 },
+ { n: '826', naam: 'Kaneel', kh_per100: 56, minPortie: 1, maxPortie: 3 },
+ ],
+ chocolate: [
+ { n: '432', naam: 'Pure chocolade', kh_per100: 46.7, minPortie: 8, maxPortie: 18 },
+ ],
+ };
+
+ // ===== Inspiratie recipe templates (variable slots) =====
+ const INSPIRATIE_TEMPLATES = [
+ {
+ naamTpl: function(fruit, topping) { return fruit.naam + '-' + topping.naam + ' bowl'; },
+ slots: [
+ { pool: 'base', desc: 'basis', varName: 'base' },
+ { pool: 'fruit', desc: 'fruit', varName: 'fruit' },
+ { pool: 'topping', desc: 'topping', varName: 'topping' },
+ ],
+ bereidingTpl: function(base, fruit, topping) {
+ return 'Schep de ' + base.naam.toLowerCase() + ' in een kom. Was de ' + fruit.naam.toLowerCase() + ' en verdeel ze erover. Bestrooi met ' + topping.naam.toLowerCase() + ' en serveer.';
+ },
+ },
+ {
+ naamTpl: function(base, fruit, flavor) { return fruit.naam + ' ' + base.naam + ' met ' + flavor.naam; },
+ slots: [
+ { pool: 'base', desc: 'basis', varName: 'base' },
+ { pool: 'fruit', desc: 'fruit', varName: 'fruit' },
+ { pool: 'flavor', desc: 'smaak', varName: 'flavor' },
+ ],
+ bereidingTpl: function(base, fruit, flavor) {
+ return 'Meng de ' + base.naam.toLowerCase() + ' met ' + flavor.naam.toLowerCase() + '. Was de ' + fruit.naam.toLowerCase() + ' en schep ze erdoor. Serveer gekoeld.';
+ },
+ },
+ {
+ naamTpl: function(base, fruit, topping) { return 'Romige ' + fruit.naam + '-' + base.naam + ' met ' + topping.naam; },
+ slots: [
+ { pool: 'base', desc: 'basis', varName: 'base' },
+ { pool: 'fruit', desc: 'fruit', varName: 'fruit' },
+ { pool: 'topping', desc: 'topping', varName: 'topping' },
+ ],
+ bereidingTpl: function(base, fruit, topping) {
+ return 'Doe de ' + base.naam.toLowerCase() + ' in een kom. Snijd de ' + fruit.naam.toLowerCase() + ' en roer erdoor. Bestrooi met ' + topping.naam.toLowerCase() + ' en geniet.';
+ },
+ },
+ {
+ naamTpl: function(base, fruit, chocolate) { return fruit.naam + '-chocolade ' + base.naam; },
+ slots: [
+ { pool: 'base', desc: 'basis', varName: 'base' },
+ { pool: 'fruit', desc: 'fruit', varName: 'fruit' },
+ { pool: 'chocolate', desc: 'chocolade', varName: 'chocolate' },
+ ],
+ bereidingTpl: function(base, fruit, chocolate) {
+ return 'Schep de ' + base.naam.toLowerCase() + ' in een kom. Was de ' + fruit.naam.toLowerCase() + ' en verdeel ze erover. Rasp de ' + chocolate.naam.toLowerCase() + ' erboven en serveer direct.';
+ },
+ },
+ {
+ naamTpl: function(base, fruit, topping) { return 'Frisse ' + fruit.naam + ' ' + base.naam + ' met ' + topping.naam; },
+ slots: [
+ { pool: 'base', desc: 'basis', varName: 'base' },
+ { pool: 'fruit', desc: 'fruit', varName: 'fruit' },
+ { pool: 'topping', desc: 'topping', varName: 'topping' },
+ ],
+ bereidingTpl: function(base, fruit, topping) {
+ return 'Meng de ' + base.naam.toLowerCase() + ' met de ' + fruit.naam.toLowerCase() + '. Hak de ' + topping.naam.toLowerCase() + ' grof en strooi erover. Direct serveren.';
+ },
+ },
+ {
+ naamTpl: function(base, fruit, flavor) { return 'Warme ' + fruit.naam + ' met ' + base.naam + ' en ' + flavor.naam; },
+ slots: [
+ { pool: 'base', desc: 'basis', varName: 'base' },
+ { pool: 'fruit', desc: 'fruit', varName: 'fruit' },
+ { pool: 'flavor', desc: 'smaak', varName: 'flavor' },
+ ],
+ bereidingTpl: function(base, fruit, flavor) {
+ return 'Verwarm de ' + fruit.naam.toLowerCase() + ' kort in een pannetje. Schep de ' + base.naam.toLowerCase() + ' in een kom, voeg de warme ' + fruit.naam.toLowerCase() + ' toe en bestrooi met ' + flavor.naam.toLowerCase() + '. Lauwwarm serveren.';
+ },
+ },
+ ];
+
+ /**
+ * Pick a random element from an array.
+ */
+ function pickRandom(arr) {
+ return arr[Math.floor(Math.random() * arr.length)];
+ }
+
+ /**
+ * Shuffle an array in place (Fisher-Yates).
+ */
+ function shuffleArray(arr) {
+ for (let i = arr.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [arr[i], arr[j]] = [arr[j], arr[i]];
+ }
+ return arr;
+ }
+
+ /**
+ * Calculate the kh for a given ingredient and portion.
+ */
+ function calcIngKh(khPer100, portie) {
+ return Math.round((khPer100 / 100) * portie * 10) / 10;
+ }
+
+ /**
+ * Generate one recipe suggestion from a random template + random ingredient choices,
+ * aiming to stay within [minVal, maxVal] carbs.
+ * Returns null if no valid combination found after maxAttempts.
+ */
+ function generateSingleSuggestion(minVal, maxVal, existingNames) {
+ const MAX_ATTEMPTS = 30;
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
+ const tpl = pickRandom(INSPIRATIE_TEMPLATES);
+ const chosen = {};
+ let valid = true;
+ let anyFromDb = false; // track whether at least one ingredient was resolved from DB
+
+ for (const slot of tpl.slots) {
+ const pool = INSPIRATIE_POOL[slot.pool];
+ if (!pool || pool.length === 0) { valid = false; break; }
+ // Shuffle and pick first that avoids name repetition within this suggestion
+ const shuffled = shuffleArray([...pool]);
+ let picked = null;
+ for (const candidate of shuffled) {
+ // Resolve each candidate via the actual NEVO database
+ const dbItem = findFoodByN(candidate.n);
+ if (!dbItem) continue; // skip IDs not found in the database
+ const nameFromDb = displayNaam(dbItem.naam);
+ const nameLower = nameFromDb.toLowerCase();
+ const alreadyUsed = Object.values(chosen).some(v => v && v.dbNaam && v.dbNaam.toLowerCase() === nameLower);
+ if (!alreadyUsed) {
+ // Build resolved ingredient: kh from DB, portions from pool metadata.
+ // Use candidate.naam (friendly name) for recipe titles and preparation text;
+ // store dbNaam separately for the ingredient list display.
+ picked = {
+ n: candidate.n,
+ naam: candidate.naam, // friendly name for templates (title + bereiding)
+ dbNaam: nameFromDb, // DB display name for ingredient list
+ khPer100: parseKh(dbItem.kh),
+ minPortie: candidate.minPortie,
+ maxPortie: candidate.maxPortie,
+ };
+ anyFromDb = true;
+ break;
+ }
+ }
+ if (!picked) { valid = false; break; }
+ chosen[slot.varName] = picked;
+ }
+
+ if (!valid) continue;
+
+ // Assign portions (using DB-backed khPer100)
+ const ingredients = [];
+ let totaalKh = 0;
+ for (const slot of tpl.slots) {
+ const ing = chosen[slot.varName];
+ const portie = ing.minPortie + Math.floor(Math.random() * (ing.maxPortie - ing.minPortie + 1));
+ const kh = calcIngKh(ing.khPer100, portie);
+ ingredients.push({ n: ing.n, naam: ing.dbNaam, portie: portie, kh: kh, khPer100: ing.khPer100 });
+ totaalKh += kh;
+ }
+ totaalKh = Math.round(totaalKh * 10) / 10;
+
+ // Check range
+ if (totaalKh < minVal || totaalKh > maxVal) continue;
+
+ // Check not identical to recently shown
+ const suggestionName = tpl.naamTpl(...Object.values(chosen));
+ if (existingNames && existingNames.includes(suggestionName)) continue;
+
+ // Build ingredient details (all values already from DB)
+ const ingredientDetails = ingredients.map(ing => ({
+ n: ing.n,
+ displayNaam: ing.naam,
+ portie: ing.portie,
+ kh: ing.kh,
+ khPer100: ing.khPer100,
+ }));
+
+ // Build bereiding text
+ const bereiding = tpl.bereidingTpl(...Object.values(chosen));
+
+ return {
+ naam: suggestionName,
+ ingredientDetails: ingredientDetails,
+ totaalKh: totaalKh,
+ bereiding: bereiding,
+ anyFromDb: anyFromDb,
+ };
+ }
+ return null; // could not generate within range
+ }
+
+ /**
+ * Generate a set of up to 3 suggestions within the given carb range.
+ * Returns an array of suggestion objects (may be empty).
+ */
+ function generateInspiratieSet(minVal, maxVal) {
+ const set = [];
+ const usedNames = new Set();
+
+ // Include existing recipes as part of the pool β at most 1 per set,
+ // so generated suggestions get a chance to appear.
+ const existingInRange = TUSSENDOORTJES_RECIPES.filter(r => {
+ let totaal = 0;
+ r.ingredienten.forEach(ing => {
+ const item = findFoodByN(ing.n);
+ if (item) totaal += (parseKh(item.kh) / 100) * ing.portie;
+ });
+ totaal = Math.round(totaal * 10) / 10;
+ return totaal >= minVal && totaal <= maxVal;
+ });
+ shuffleArray(existingInRange);
+
+ // Pick at most 1 existing recipe
+ if (existingInRange.length > 0) {
+ const r = existingInRange[0];
+ let totaalKh = 0;
+ const ingredientDetails = r.ingredienten.map(ing => {
+ const item = findFoodByN(ing.n);
+ const khPer100 = item ? parseKh(item.kh) : 0;
+ const khPortie = Math.round((khPer100 / 100) * ing.portie * 10) / 10;
+ totaalKh += khPortie;
+ return { n: ing.n, displayNaam: item ? displayNaam(item.naam) : ing.naam, portie: ing.portie, kh: khPortie, khPer100: khPer100 };
+ });
+ totaalKh = Math.round(totaalKh * 10) / 10;
+ if (totaalKh >= minVal && totaalKh <= maxVal) {
+ usedNames.add(r.naam);
+ set.push({ naam: r.naam, ingredientDetails, totaalKh, bereiding: r.bereiding, fromPool: true });
+ }
+ }
+
+ // Generate remaining suggestions
+ const MAX_GEN_ATTEMPTS = 50;
+ for (let attempt = 0; attempt < MAX_GEN_ATTEMPTS && set.length < 3; attempt++) {
+ const suggestion = generateSingleSuggestion(minVal, maxVal, Array.from(usedNames));
+ if (suggestion) {
+ usedNames.add(suggestion.naam);
+ set.push({ ...suggestion, fromPool: false });
+ }
+ }
+
+ return set;
+ }
const CATEGORY_MAP = {
'Brood': 'Brood',
'Melk en melkproducten': 'Zuivel',
@@ -3700,10 +4350,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
return `
-
+
π½ ${escapeHtml(entry.mealNaam)}${formatKh(mealKh)} g
-
+
Lekkere, koolhydraatarme snacks binnen jouw gekozen bereik
+
+
+
+ Koolhydraten per portie
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Belangrijk: De getoonde voedingswaarden zijn gebaseerd op de NEVO-database (RIVM) en kunnen afwijken van merkproducten. Controleer altijd het etiket van jouw eigen producten. Deze suggesties zijn geen medisch advies en vervangen geen begeleiding van een diΓ«tist of arts.
+
+
+
@@ -2666,7 +3011,312 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
const CATEGORIES = ['Brood', 'Zuivel', 'Fruit', 'Groente', 'Vlees/Vis', 'Dranken', 'Tussendoortjes', 'Snoep/Koek', 'Overig'];
- // Map NEVO categories to app categories for filtering
+ // ===== Tussendoortjes Recepten =====
+ const TUSSENDOORTJES_RECIPES = [
+ {
+ naam: 'Chocolade-Frambozenbowl',
+ bereiding: 'Schep de kwark in een kom en meng de cacaopoeder er glad door. Was de frambozen en verdeel ze over de kwark. Rasp de pure chocolade erover en serveer direct.',
+ ingredienten: [
+ { n: '305', naam: 'Kwark magere', portie: 175 },
+ { n: '161', naam: 'Frambozen', portie: 50 },
+ { n: '430', naam: 'Cacaopoeder', portie: 8 },
+ { n: '432', naam: 'Pure chocolade', portie: 15 },
+ ],
+ },
+ {
+ naam: 'Aardbei-Chocoladekwark',
+ bereiding: 'Was de aardbeien en snijd ze in plakjes. Meng de kwark met de cacaopoeder. Schep de aardbeien erdoor. Rasp de pure chocolade erover en geniet.',
+ ingredienten: [
+ { n: '305', naam: 'Kwark magere', portie: 150 },
+ { n: '148', naam: 'Aardbeien', portie: 100 },
+ { n: '432', naam: 'Pure chocolade', portie: 18 },
+ ],
+ },
+ {
+ naam: 'Warme Appel-Kaneelkwark',
+ bereiding: 'Schil de appel, verwijder het klokhuis en snijd in kleine blokjes. Doe in een pannetje met een scheutje water en de kaneel. Verwarm 3 minuten op middelhoog vuur. Schep de warme appel door de kwark en serveer lauwwarm.',
+ ingredienten: [
+ { n: '305', naam: 'Kwark magere', portie: 180 },
+ { n: '147', naam: 'Appel', portie: 100 },
+ { n: '826', naam: 'Kaneel', portie: 2 },
+ ],
+ },
+ ];
+
+ // ===== Inspiratie (Vind inspiratie) state =====
+ let _inspiratieMoment = null; // eetmoment id: 'Tussendoor 1/2/3'
+ let _inspiratieDate = null; // date string from diary
+ let _lastSuggestieIds = []; // track last set of names to avoid immediate repeats
+ let _cachedGeneratedSet = null; // current generated set (array of generated recipe objects)
+
+ // ===== Inspiratie ingredient pool (NEVO-based) =====
+ const INSPIRATIE_POOL = {
+ base: [
+ { n: '305', naam: 'Kwark magere', kh_per100: 3.8, eiwit_per100: 8.4, minPortie: 125, maxPortie: 200 },
+ { n: '306', naam: 'Kwark halfvolle', kh_per100: 4.0, eiwit_per100: 7.5, minPortie: 125, maxPortie: 200 },
+ { n: '654', naam: 'Cottage cheese', kh_per100: 2.3, eiwit_per100: 11.2, minPortie: 125, maxPortie: 200 },
+ { n: '2503', naam: 'Griekse yoghurt volle', kh_per100: 3.8, eiwit_per100: 3.8, minPortie: 125, maxPortie: 175 },
+ ],
+ fruit: [
+ { n: '161', naam: 'Frambozen', kh_per100: 4.5, minPortie: 40, maxPortie: 75 },
+ { n: '148', naam: 'Aardbeien', kh_per100: 5.1, minPortie: 50, maxPortie: 100 },
+ { n: '152', naam: 'Blauwe bessen', kh_per100: 11, minPortie: 30, maxPortie: 60 },
+ { n: '155', naam: 'Bosbessen', kh_per100: 6, minPortie: 40, maxPortie: 75 },
+ { n: '157', naam: 'Bramen', kh_per100: 5.1, minPortie: 40, maxPortie: 75 },
+ { n: '147', naam: 'Appel', kh_per100: 12, minPortie: 50, maxPortie: 100 },
+ ],
+ topping: [
+ { n: '198', naam: 'Amandelen', kh_per100: 7.1, minPortie: 8, maxPortie: 15 },
+ { n: '206', naam: 'Walnoten', kh_per100: 5.1, minPortie: 8, maxPortie: 15 },
+ { n: '1895', naam: 'Pecannoten', kh_per100: 4.4, minPortie: 8, maxPortie: 15 },
+ { n: '5497', naam: 'Kokos geraspt', kh_per100: 8.6, minPortie: 5, maxPortie: 12 },
+ { n: '3447', naam: 'Chiazaad', kh_per100: 7.7, minPortie: 5, maxPortie: 10 },
+ ],
+ flavor: [
+ { n: '430', naam: 'Cacaopoeder', kh_per100: 10.5, minPortie: 5, maxPortie: 10 },
+ { n: '826', naam: 'Kaneel', kh_per100: 56, minPortie: 1, maxPortie: 3 },
+ ],
+ chocolate: [
+ { n: '432', naam: 'Pure chocolade', kh_per100: 46.7, minPortie: 8, maxPortie: 18 },
+ ],
+ };
+
+ // ===== Inspiratie recipe templates (variable slots) =====
+ const INSPIRATIE_TEMPLATES = [
+ {
+ naamTpl: function(fruit, topping) { return fruit.naam + '-' + topping.naam + ' bowl'; },
+ slots: [
+ { pool: 'base', desc: 'basis', varName: 'base' },
+ { pool: 'fruit', desc: 'fruit', varName: 'fruit' },
+ { pool: 'topping', desc: 'topping', varName: 'topping' },
+ ],
+ bereidingTpl: function(base, fruit, topping) {
+ return 'Schep de ' + base.naam.toLowerCase() + ' in een kom. Was de ' + fruit.naam.toLowerCase() + ' en verdeel ze erover. Bestrooi met ' + topping.naam.toLowerCase() + ' en serveer.';
+ },
+ },
+ {
+ naamTpl: function(base, fruit, flavor) { return fruit.naam + ' ' + base.naam + ' met ' + flavor.naam; },
+ slots: [
+ { pool: 'base', desc: 'basis', varName: 'base' },
+ { pool: 'fruit', desc: 'fruit', varName: 'fruit' },
+ { pool: 'flavor', desc: 'smaak', varName: 'flavor' },
+ ],
+ bereidingTpl: function(base, fruit, flavor) {
+ return 'Meng de ' + base.naam.toLowerCase() + ' met ' + flavor.naam.toLowerCase() + '. Was de ' + fruit.naam.toLowerCase() + ' en schep ze erdoor. Serveer gekoeld.';
+ },
+ },
+ {
+ naamTpl: function(base, fruit, topping) { return 'Romige ' + fruit.naam + '-' + base.naam + ' met ' + topping.naam; },
+ slots: [
+ { pool: 'base', desc: 'basis', varName: 'base' },
+ { pool: 'fruit', desc: 'fruit', varName: 'fruit' },
+ { pool: 'topping', desc: 'topping', varName: 'topping' },
+ ],
+ bereidingTpl: function(base, fruit, topping) {
+ return 'Doe de ' + base.naam.toLowerCase() + ' in een kom. Snijd de ' + fruit.naam.toLowerCase() + ' en roer erdoor. Bestrooi met ' + topping.naam.toLowerCase() + ' en geniet.';
+ },
+ },
+ {
+ naamTpl: function(base, fruit, chocolate) { return fruit.naam + '-chocolade ' + base.naam; },
+ slots: [
+ { pool: 'base', desc: 'basis', varName: 'base' },
+ { pool: 'fruit', desc: 'fruit', varName: 'fruit' },
+ { pool: 'chocolate', desc: 'chocolade', varName: 'chocolate' },
+ ],
+ bereidingTpl: function(base, fruit, chocolate) {
+ return 'Schep de ' + base.naam.toLowerCase() + ' in een kom. Was de ' + fruit.naam.toLowerCase() + ' en verdeel ze erover. Rasp de ' + chocolate.naam.toLowerCase() + ' erboven en serveer direct.';
+ },
+ },
+ {
+ naamTpl: function(base, fruit, topping) { return 'Frisse ' + fruit.naam + ' ' + base.naam + ' met ' + topping.naam; },
+ slots: [
+ { pool: 'base', desc: 'basis', varName: 'base' },
+ { pool: 'fruit', desc: 'fruit', varName: 'fruit' },
+ { pool: 'topping', desc: 'topping', varName: 'topping' },
+ ],
+ bereidingTpl: function(base, fruit, topping) {
+ return 'Meng de ' + base.naam.toLowerCase() + ' met de ' + fruit.naam.toLowerCase() + '. Hak de ' + topping.naam.toLowerCase() + ' grof en strooi erover. Direct serveren.';
+ },
+ },
+ {
+ naamTpl: function(base, fruit, flavor) { return 'Warme ' + fruit.naam + ' met ' + base.naam + ' en ' + flavor.naam; },
+ slots: [
+ { pool: 'base', desc: 'basis', varName: 'base' },
+ { pool: 'fruit', desc: 'fruit', varName: 'fruit' },
+ { pool: 'flavor', desc: 'smaak', varName: 'flavor' },
+ ],
+ bereidingTpl: function(base, fruit, flavor) {
+ return 'Verwarm de ' + fruit.naam.toLowerCase() + ' kort in een pannetje. Schep de ' + base.naam.toLowerCase() + ' in een kom, voeg de warme ' + fruit.naam.toLowerCase() + ' toe en bestrooi met ' + flavor.naam.toLowerCase() + '. Lauwwarm serveren.';
+ },
+ },
+ ];
+
+ /**
+ * Pick a random element from an array.
+ */
+ function pickRandom(arr) {
+ return arr[Math.floor(Math.random() * arr.length)];
+ }
+
+ /**
+ * Shuffle an array in place (Fisher-Yates).
+ */
+ function shuffleArray(arr) {
+ for (let i = arr.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [arr[i], arr[j]] = [arr[j], arr[i]];
+ }
+ return arr;
+ }
+
+ /**
+ * Calculate the kh for a given ingredient and portion.
+ */
+ function calcIngKh(khPer100, portie) {
+ return Math.round((khPer100 / 100) * portie * 10) / 10;
+ }
+
+ /**
+ * Generate one recipe suggestion from a random template + random ingredient choices,
+ * aiming to stay within [minVal, maxVal] carbs.
+ * Returns null if no valid combination found after maxAttempts.
+ */
+ function generateSingleSuggestion(minVal, maxVal, existingNames) {
+ const MAX_ATTEMPTS = 30;
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
+ const tpl = pickRandom(INSPIRATIE_TEMPLATES);
+ const chosen = {};
+ let valid = true;
+ let anyFromDb = false; // track whether at least one ingredient was resolved from DB
+
+ for (const slot of tpl.slots) {
+ const pool = INSPIRATIE_POOL[slot.pool];
+ if (!pool || pool.length === 0) { valid = false; break; }
+ // Shuffle and pick first that avoids name repetition within this suggestion
+ const shuffled = shuffleArray([...pool]);
+ let picked = null;
+ for (const candidate of shuffled) {
+ // Resolve each candidate via the actual NEVO database
+ const dbItem = findFoodByN(candidate.n);
+ if (!dbItem) continue; // skip IDs not found in the database
+ const nameFromDb = displayNaam(dbItem.naam);
+ const nameLower = nameFromDb.toLowerCase();
+ const alreadyUsed = Object.values(chosen).some(v => v && v.dbNaam && v.dbNaam.toLowerCase() === nameLower);
+ if (!alreadyUsed) {
+ // Build resolved ingredient: kh from DB, portions from pool metadata.
+ // Use candidate.naam (friendly name) for recipe titles and preparation text;
+ // store dbNaam separately for the ingredient list display.
+ picked = {
+ n: candidate.n,
+ naam: candidate.naam, // friendly name for templates (title + bereiding)
+ dbNaam: nameFromDb, // DB display name for ingredient list
+ khPer100: parseKh(dbItem.kh),
+ minPortie: candidate.minPortie,
+ maxPortie: candidate.maxPortie,
+ };
+ anyFromDb = true;
+ break;
+ }
+ }
+ if (!picked) { valid = false; break; }
+ chosen[slot.varName] = picked;
+ }
+
+ if (!valid) continue;
+
+ // Assign portions (using DB-backed khPer100)
+ const ingredients = [];
+ let totaalKh = 0;
+ for (const slot of tpl.slots) {
+ const ing = chosen[slot.varName];
+ const portie = ing.minPortie + Math.floor(Math.random() * (ing.maxPortie - ing.minPortie + 1));
+ const kh = calcIngKh(ing.khPer100, portie);
+ ingredients.push({ n: ing.n, naam: ing.dbNaam, portie: portie, kh: kh, khPer100: ing.khPer100 });
+ totaalKh += kh;
+ }
+ totaalKh = Math.round(totaalKh * 10) / 10;
+
+ // Check range
+ if (totaalKh < minVal || totaalKh > maxVal) continue;
+
+ // Check not identical to recently shown
+ const suggestionName = tpl.naamTpl(...Object.values(chosen));
+ if (existingNames && existingNames.includes(suggestionName)) continue;
+
+ // Build ingredient details (all values already from DB)
+ const ingredientDetails = ingredients.map(ing => ({
+ n: ing.n,
+ displayNaam: ing.naam,
+ portie: ing.portie,
+ kh: ing.kh,
+ khPer100: ing.khPer100,
+ }));
+
+ // Build bereiding text
+ const bereiding = tpl.bereidingTpl(...Object.values(chosen));
+
+ return {
+ naam: suggestionName,
+ ingredientDetails: ingredientDetails,
+ totaalKh: totaalKh,
+ bereiding: bereiding,
+ anyFromDb: anyFromDb,
+ };
+ }
+ return null; // could not generate within range
+ }
+
+ /**
+ * Generate a set of up to 3 suggestions within the given carb range.
+ * Returns an array of suggestion objects (may be empty).
+ */
+ function generateInspiratieSet(minVal, maxVal) {
+ const set = [];
+ const usedNames = new Set();
+
+ // Include existing recipes as part of the pool β at most 1 per set,
+ // so generated suggestions get a chance to appear.
+ const existingInRange = TUSSENDOORTJES_RECIPES.filter(r => {
+ let totaal = 0;
+ r.ingredienten.forEach(ing => {
+ const item = findFoodByN(ing.n);
+ if (item) totaal += (parseKh(item.kh) / 100) * ing.portie;
+ });
+ totaal = Math.round(totaal * 10) / 10;
+ return totaal >= minVal && totaal <= maxVal;
+ });
+ shuffleArray(existingInRange);
+
+ // Pick at most 1 existing recipe
+ if (existingInRange.length > 0) {
+ const r = existingInRange[0];
+ let totaalKh = 0;
+ const ingredientDetails = r.ingredienten.map(ing => {
+ const item = findFoodByN(ing.n);
+ const khPer100 = item ? parseKh(item.kh) : 0;
+ const khPortie = Math.round((khPer100 / 100) * ing.portie * 10) / 10;
+ totaalKh += khPortie;
+ return { n: ing.n, displayNaam: item ? displayNaam(item.naam) : ing.naam, portie: ing.portie, kh: khPortie, khPer100: khPer100 };
+ });
+ totaalKh = Math.round(totaalKh * 10) / 10;
+ if (totaalKh >= minVal && totaalKh <= maxVal) {
+ usedNames.add(r.naam);
+ set.push({ naam: r.naam, ingredientDetails, totaalKh, bereiding: r.bereiding, fromPool: true });
+ }
+ }
+
+ // Generate remaining suggestions
+ const MAX_GEN_ATTEMPTS = 50;
+ for (let attempt = 0; attempt < MAX_GEN_ATTEMPTS && set.length < 3; attempt++) {
+ const suggestion = generateSingleSuggestion(minVal, maxVal, Array.from(usedNames));
+ if (suggestion) {
+ usedNames.add(suggestion.naam);
+ set.push({ ...suggestion, fromPool: false });
+ }
+ }
+
+ return set;
+ }
const CATEGORY_MAP = {
'Brood': 'Brood',
'Melk en melkproducten': 'Zuivel',
@@ -3700,10 +4350,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
return `
-
+
π½ ${escapeHtml(entry.mealNaam)}${formatKh(mealKh)} g
-
+