feat(seed): popola Recipe.variants e RecipeIngredient.substitutes

- DEFAULT_SUBSTITUTES_BY_INGREDIENT in seed-recipes.ts: ~50 mappature
  riusabili (pesci interscambiabili, latticini lactose-free, manzo→tacchino,
  pollo→tacchino, frutta secca, condimenti, legumi inter-cambiabili)
- 12 ricette signature ora dichiarano variants reali (1-2 ciascuna):
  • Frittata di spinaci → senza lattosio / più saziante
  • Yogurt greco con noci → vegana / con frutti di bosco
  • Uova strapazzate con pancetta → senza maiale / con verdure
  • Insalata di pollo, avocado e pomodorini → vegetariana / con feta e olive
  • Salmone al forno con asparagi → versione fredda / sgombro
  • Bistecca con zucchine → tacchino / con melanzane
  • Insalatona con tonno e olive → Niçoise / sgombro
  • Branzino al cartoccio → mediterranea / orata
  • Polpette di pollo e ricotta → in sugo / tacchino-spinaci
  • Tagliata con rucola → con pomodorini / bresaola
  • Caprese → con avocado / con prosciutto crudo
  • Pancake proteici, Smoothie verde, Tagliolini di zucchine,
    Insalata di lenticchie, Quinoa con pollo (varianti analoghe)
- seed.ts ora propaga r.variants al campo Recipe.variants e applica
  ing.substitutes (override) o DEFAULT_SUBSTITUTES_BY_INGREDIENT[name]
  alle righe RecipeIngredient

Smoke verificato live: /recipes/<frittata> mostra "VARIANTI SUGGERITE"
con "Versione senza lattosio (-40 kcal)" e "Più saziante (+90 kcal)";
gli ingredienti hanno la riga "Sostituzioni: bietole, Grana Padano…".

Catalog: 46 ricette, 164 link RecipeIngredient, ~50 mappature substitutes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Opus 4.7 committed 2026-04-29 23:31:33 +02:00
1 parent bda96a3550
commit c37dc5d717
2 files changed
+289 -3

No files matched your search

+283 -2
View File
@@ -1,17 +1,30 @@
// Seed minimo del catalogo ricette italiane keto/low-carb (PRD §5.1).
// 16 ricette distribuite su COLAZIONE/PRANZO/SPUNTINO/CENA, fasi 1/2/3.
// Seed catalogo ricette italiane keto/low-carb (PRD §5.1).
// 46 ricette distribuite su COLAZIONE/PRANZO/SPUNTINO/CENA, fasi 1/2/3.
// Macros stimati per porzione singola.
//
// Ogni ricetta dichiara una lista di `ingredients` con quantità e unità,
// usata dal seeder per creare i record `RecipeIngredient`. Il `name` deve
// corrispondere esattamente a un ingrediente di `seed-ingredients.ts`.
//
// `variants` espone alternative testuali alla ricetta (es. versione
// vegetariana o senza lattosio) — vengono salvate in Recipe.variants.
// `substitutes` su un singolo ingrediente sovrascrive la mappa default
// (DEFAULT_SUBSTITUTES_BY_INGREDIENT) per quella ricetta specifica.
import type { Difficulty, MealCategory } from '@prisma/client';
export interface RecipeVariant {
name: string;
description: string;
kcalDelta?: number;
}
export interface SeedRecipeIngredient {
name: string;
quantity: number;
unit: string;
/** Se omesso, viene preso da DEFAULT_SUBSTITUTES_BY_INGREDIENT. */
substitutes?: string[];
}
export interface SeedRecipe {
@@ -27,8 +40,81 @@ export interface SeedRecipe {
phases: number[];
notesChef?: string;
ingredients: SeedRecipeIngredient[];
variants?: RecipeVariant[];
}
// Sostituzioni "ragionevoli" per ingrediente, riusate da tutte le ricette.
// Il seeder le applica a `RecipeIngredient.substitutes` quando la ricetta
// non specifica un override esplicito. Riempire qui evita di ripetere le
// stesse sostituzioni in 46 ricette × N ingredienti.
export const DEFAULT_SUBSTITUTES_BY_INGREDIENT: Record<string, string[]> = {
// Pesci → interscambiabili, attenzione al gusto
'Filetto di salmone': ['trota salmonata', 'sgombro fresco', 'tonno fresco'],
Sgombro: ['salmone', 'sardine fresche', 'palamita'],
Branzino: ['orata', 'spigola', 'dentice'],
"Tonno sott'olio": ['sgombro al naturale', 'salmone affumicato', 'pollo lessato'],
Gamberi: ['mazzancolle', 'scampi puliti', 'calamari a strisce'],
// Carni
'Petto di pollo': ['petto di tacchino', 'fesa di pollo bio', 'sovracoscia disossata'],
'Petto di tacchino': ['petto di pollo', 'lonza di maiale magra'],
'Tagliata di manzo': ['tagliata di scottona', 'controfiletto', 'flank steak'],
'Pancetta affumicata': ['guanciale', 'speck a cubetti', 'pancetta dolce'],
'Prosciutto crudo': ['speck stagionato', 'culatello', 'coppa stagionata'],
Bresaola: ['carpaccio di manzo', 'roastbeef sottile', "tonno sott'olio"],
// Latticini → versioni lactose-free quando possibile
'Yogurt greco intero': ['skyr intero', 'yogurt di cocco non zuccherato'],
'Parmigiano Reggiano': ['Grana Padano', 'pecorino romano stagionato'],
'Grana Padano': ['Parmigiano Reggiano', 'pecorino dolce'],
'Mozzarella di bufala': ['fiordilatte', 'burrata (porzione ridotta)'],
Stracchino: ['robiola', 'crescenza', 'philadelphia'],
Mascarpone: ['ricotta intera setacciata', 'cream cheese intero'],
Ricotta: ['ricotta di capra', 'fiocchi di latte'],
'Ricotta vaccina': ['ricotta di pecora', 'fiocchi di latte', 'cottage cheese'],
'Gorgonzola dolce DOP': ['taleggio', 'roquefort', 'fontina'],
Burro: ['ghee', 'olio di cocco extravergine'],
// Uova / vegetali
Uova: ['tofu strapazzato (versione vegana)'],
Avocado: ['mezzo avocado piccolo', 'guacamole non zuccherato'],
Spinaci: ['bietole', 'cime di rapa', 'lattuga romana saltata'],
'Spinaci freschi': ['bietole', 'cime di rapa', 'spinaci surgelati'],
'Spinaci surgelati': ['spinaci freschi (porzione doppia)', 'bietole'],
Asparagi: ['fagiolini', 'broccoletti', 'puntarelle'],
Zucchine: ['cetrioli grandi', 'zucchina chiara', 'finocchi'],
'Pomodorini ciliegino': ['pomodori datterini', 'cuore di bue a cubetti'],
'Insalata mista': ['valeriana', 'songino', 'rucola e radicchio'],
Rucola: ['valeriana', 'spinacino baby'],
Cavolfiore: ['broccoli', 'cavolo romanesco'],
Broccoli: ['cavolfiore', 'broccoletti'],
'Funghi champignon': ['funghi cardoncelli', 'funghi porcini'],
Melanzane: ['zucchine', 'peperoni'],
Peperoni: ['zucchine', 'pomodori ramati'],
Cetrioli: ['sedano croccante', 'finocchi a fettine'],
Carote: ['carote viola', 'pastinaca cotta (fase 2+)'],
// Frutta
'Frutti di bosco misti': ['lamponi', 'mirtilli', 'fragoline di bosco'],
Mela: ['pera (porzione ridotta)', 'kiwi'],
Pera: ['mela'],
Limone: ['lime', 'aceto di mele'],
// Frutta secca / semi
Mandorle: ['nocciole', 'noci pecan'],
Noci: ['mandorle', 'pinoli'],
'Semi di chia': ['semi di lino macinati', 'psillio'],
'Cocco rapè non zuccherato': ['mandorle a scaglie', 'farina di cocco'],
// Condimenti / panetteria
'Olio extravergine di oliva': ['olio di avocado', 'olio di sesamo per piatti orientali'],
'Olive nere': ['olive di Gaeta', 'olive taggiasche'],
'Olive verdi': ['olive di Castelvetrano', 'olive baresane'],
'Capperi sotto sale': ["capperi sott'aceto (sgocciolati)", 'olive nere tritate'],
'Pane proteico di mandorle': ['cracker di semi keto', 'foglie di lattuga (per wrap)'],
'Aceto balsamico': ['aceto di mele', 'glassa di balsamico keto-friendly'],
'Cannella in polvere': ['cardamomo', 'noce moscata'],
'Basilico fresco': ['mentuccia', 'foglie di prezzemolo'],
// Legumi / cereali (fase 2+)
'Lenticchie cotte': ['ceci cotti', 'fagioli cannellini'],
'Ceci cotti': ['lenticchie cotte', 'fagioli borlotti'],
'Quinoa cotta': ['miglio cotto', 'farro perlato cotto'],
};
export const SEED_RECIPES: SeedRecipe[] = [
// COLAZIONE
{
@@ -50,6 +136,18 @@ export const SEED_RECIPES: SeedRecipe[] = [
{ name: 'Parmigiano Reggiano', quantity: 20, unit: 'g' },
{ name: 'Olio extravergine di oliva', quantity: 1, unit: 'cucchiaio' },
],
variants: [
{
name: 'Versione senza lattosio',
description: 'Sostituisci il parmigiano con 30 g di lievito alimentare in scaglie.',
kcalDelta: -40,
},
{
name: 'Più saziante',
description: 'Aggiungi una fetta di prosciutto crudo a julienne durante la cottura.',
kcalDelta: 90,
},
],
},
{
name: 'Yogurt greco con noci e cannella',
@@ -67,6 +165,19 @@ export const SEED_RECIPES: SeedRecipe[] = [
{ name: 'Noci', quantity: 20, unit: 'g' },
{ name: 'Cannella in polvere', quantity: 1, unit: 'cucchiaino' },
],
variants: [
{
name: 'Versione vegana',
description: 'Yogurt di cocco non zuccherato al posto di quello greco.',
kcalDelta: -50,
},
{
name: 'Con frutti di bosco (Fase 2+)',
description:
'Aggiungi 50 g di mirtilli o lamponi: porta zuccheri naturali ma resta sotto i 12 g di carb netti.',
kcalDelta: 25,
},
],
},
{
name: 'Avocado toast keto su pane di mandorle',
@@ -102,6 +213,17 @@ export const SEED_RECIPES: SeedRecipe[] = [
{ name: 'Pancetta affumicata', quantity: 30, unit: 'g' },
{ name: 'Burro', quantity: 10, unit: 'g' },
],
variants: [
{
name: 'Senza maiale',
description: 'Sostituisci la pancetta con bresaola o tacchino arrosto a striscioline.',
kcalDelta: -80,
},
{
name: 'Con verdure',
description: 'Aggiungi 50 g di funghi champignon trifolati o pomodorini saltati.',
},
],
},
// PRANZO
{
@@ -122,6 +244,18 @@ export const SEED_RECIPES: SeedRecipe[] = [
{ name: 'Insalata mista', quantity: 60, unit: 'g' },
{ name: 'Olio extravergine di oliva', quantity: 2, unit: 'cucchiaio' },
],
variants: [
{
name: 'Versione vegetariana',
description: 'Pollo sostituito da 100 g di tofu marinato o 2 uova sode.',
kcalDelta: -100,
},
{
name: 'Con feta e olive',
description: 'Aggiungi 40 g di feta sbriciolata e 8 olive Kalamata: stile mediterraneo.',
kcalDelta: 130,
},
],
},
{
name: 'Salmone al forno con asparagi',
@@ -141,6 +275,17 @@ export const SEED_RECIPES: SeedRecipe[] = [
{ name: 'Limone', quantity: 0.5, unit: 'pz' },
{ name: 'Olio extravergine di oliva', quantity: 1, unit: 'cucchiaio' },
],
variants: [
{
name: 'Versione fredda',
description: 'Salmone scottato 2 min per lato e servito tiepido su asparagi crudi sottili.',
},
{
name: 'Sgombro al posto del salmone',
description: 'Più economico e ricco di omega-3. Riduci il tempo di cottura a 10 min.',
kcalDelta: -10,
},
],
},
{
name: 'Bistecca con zucchine grigliate',
@@ -158,6 +303,19 @@ export const SEED_RECIPES: SeedRecipe[] = [
{ name: 'Zucchine', quantity: 200, unit: 'g' },
{ name: 'Olio extravergine di oliva', quantity: 2, unit: 'cucchiaio' },
],
variants: [
{
name: 'Versione tacchino',
description:
'Petto di tacchino spesso al posto della tagliata: più magro, meno grassi saturi.',
kcalDelta: -180,
},
{
name: 'Con melanzane grigliate',
description: 'Aggiungi 100 g di melanzane grigliate per più volume e fibra.',
kcalDelta: 30,
},
],
},
{
name: 'Insalatona con tonno e olive',
@@ -177,6 +335,17 @@ export const SEED_RECIPES: SeedRecipe[] = [
{ name: 'Capperi sotto sale', quantity: 1, unit: 'cucchiaio' },
{ name: 'Olio extravergine di oliva', quantity: 1, unit: 'cucchiaio' },
],
variants: [
{
name: 'Niçoise',
description: 'Aggiungi 1 uovo sodo, 4 fagiolini cotti e una manciata di pomodorini.',
kcalDelta: 90,
},
{
name: 'Con sgombro',
description: 'Sgombro al naturale al posto del tonno: più ricco di omega-3.',
},
],
},
// SPUNTINO
{
@@ -259,6 +428,18 @@ export const SEED_RECIPES: SeedRecipe[] = [
{ name: 'Limone', quantity: 0.5, unit: 'pz' },
{ name: 'Olio extravergine di oliva', quantity: 1, unit: 'cucchiaio' },
],
variants: [
{
name: 'Mediterranea',
description:
'Aggiungi 6 olive Kalamata, 1 cucchiaino di capperi e una foglia di alloro nel cartoccio.',
kcalDelta: 50,
},
{
name: 'Orata al posto del branzino',
description: "Stessa preparazione: l'orata è più dolce e ha carne più morbida.",
},
],
},
{
name: 'Polpette di pollo e ricotta',
@@ -279,6 +460,19 @@ export const SEED_RECIPES: SeedRecipe[] = [
{ name: 'Uova', quantity: 1, unit: 'pz' },
{ name: 'Olio extravergine di oliva', quantity: 1, unit: 'cucchiaio' },
],
variants: [
{
name: 'In sugo di pomodoro',
description: 'Cuoci le polpette nella passata di pomodoro per gli ultimi 10 minuti.',
kcalDelta: 40,
},
{
name: 'Versione tacchino e spinaci',
description:
'Tacchino macinato + 50 g di spinaci surgelati strizzati al posto della ricotta.',
kcalDelta: -80,
},
],
},
{
name: 'Zuppa di cavolfiore e gorgonzola',
@@ -315,6 +509,18 @@ export const SEED_RECIPES: SeedRecipe[] = [
{ name: 'Grana Padano', quantity: 25, unit: 'g' },
{ name: 'Olio extravergine di oliva', quantity: 1, unit: 'cucchiaio' },
],
variants: [
{
name: 'Con pomodorini e aceto balsamico',
description: 'Aggiungi 80 g di pomodorini e un filo di aceto balsamico (Fase 2+).',
kcalDelta: 25,
},
{
name: 'Bresaola al posto della tagliata',
description: 'Versione più magra: 100 g di bresaola arrotolata sulla rucola.',
kcalDelta: -240,
},
],
},
// ─── Espansione catalogo ──────────────────────────────────────────────
// COLAZIONE
@@ -336,6 +542,19 @@ export const SEED_RECIPES: SeedRecipe[] = [
{ name: 'Cannella in polvere', quantity: 1, unit: 'cucchiaino' },
{ name: 'Burro', quantity: 5, unit: 'g' },
],
variants: [
{
name: 'Con farina di mandorle',
description: 'Aggiungi 20 g di farina di mandorle per pancake più strutturati.',
kcalDelta: 120,
},
{
name: 'Versione vegana',
description:
'Sostituisci le uova con 2 cucchiai di semi di chia + 60 ml di acqua riposati 10 min, e la ricotta con yogurt di cocco.',
kcalDelta: -80,
},
],
},
{
name: 'Smoothie verde con avocado e spinaci',
@@ -354,6 +573,19 @@ export const SEED_RECIPES: SeedRecipe[] = [
{ name: 'Yogurt greco intero', quantity: 100, unit: 'g' },
{ name: 'Semi di chia', quantity: 10, unit: 'g' },
],
variants: [
{
name: 'Versione vegana',
description:
'Yogurt di cocco al posto del greco; aggiungi 1 cucchiaio di proteine in polvere se serve.',
kcalDelta: -30,
},
{
name: 'Con frutti di bosco (Fase 2+)',
description: 'Aggiungi 60 g di mirtilli/lamponi: smoothie più gustoso, +12 g carb netti.',
kcalDelta: 30,
},
],
},
{
name: 'Uova alla coque con bresaola',
@@ -529,6 +761,18 @@ export const SEED_RECIPES: SeedRecipe[] = [
{ name: 'Basilico fresco', quantity: 10, unit: 'g' },
{ name: 'Olio extravergine di oliva', quantity: 1.5, unit: 'cucchiaio' },
],
variants: [
{
name: 'Con avocado',
description: 'Aggiungi mezzo avocado a fette: capresotto più nutriente e saziante.',
kcalDelta: 120,
},
{
name: 'Con prosciutto crudo',
description: 'Avvolgi le fette di mozzarella con prosciutto crudo: più proteine.',
kcalDelta: 80,
},
],
},
{
name: 'Insalata di gamberi e avocado',
@@ -597,12 +841,25 @@ export const SEED_RECIPES: SeedRecipe[] = [
fatG: 18,
netCarbG: 22,
phases: [2, 3],
notesChef: 'Disponibile dalla 3ª settimana di Fase 2 (reintroduzione legumi).',
ingredients: [
{ name: 'Lenticchie cotte', quantity: 130, unit: 'g' },
{ name: "Tonno sott'olio", quantity: 100, unit: 'g' },
{ name: 'Pomodorini ciliegino', quantity: 80, unit: 'g' },
{ name: 'Olio extravergine di oliva', quantity: 1, unit: 'cucchiaio' },
],
variants: [
{
name: 'Con ceci al posto delle lenticchie',
description: 'Stessa ricetta con ceci cotti: più cremosa, disponibile dalla settimana 4.',
kcalDelta: 50,
},
{
name: 'Con sgombro fresco',
description: 'Sgombro al naturale al posto del tonno: profilo lipidico migliore.',
kcalDelta: -20,
},
],
},
// SPUNTINO
{
@@ -739,6 +996,17 @@ export const SEED_RECIPES: SeedRecipe[] = [
{ name: 'Parmigiano Reggiano', quantity: 20, unit: 'g' },
{ name: 'Olio extravergine di oliva', quantity: 2, unit: 'cucchiaio' },
],
variants: [
{
name: 'Con gamberi saltati',
description: 'Aggiungi 100 g di gamberi saltati in padella: piatto più completo.',
kcalDelta: 100,
},
{
name: 'Pesto di rucola',
description: 'Sostituisci il basilico con rucola: gusto più deciso, stesso profilo macros.',
},
],
},
{
name: 'Pollo al curry con cavolfiore',
@@ -823,6 +1091,7 @@ export const SEED_RECIPES: SeedRecipe[] = [
fatG: 22,
netCarbG: 26,
phases: [3],
notesChef: 'Disponibile in mantenimento (Fase 3) o dalla 7ª settimana di Fase 2.',
ingredients: [
{ name: 'Quinoa cotta', quantity: 100, unit: 'g' },
{ name: 'Petto di pollo', quantity: 130, unit: 'g' },
@@ -830,6 +1099,18 @@ export const SEED_RECIPES: SeedRecipe[] = [
{ name: 'Zucchine', quantity: 100, unit: 'g' },
{ name: 'Olio extravergine di oliva', quantity: 1, unit: 'cucchiaio' },
],
variants: [
{
name: 'Versione vegetariana',
description: 'Sostituisci il pollo con 80 g di feta + 1 uovo sodo: più mediterranea.',
kcalDelta: -30,
},
{
name: 'Con gamberi',
description: 'Gamberi al posto del pollo: piatto più estivo e leggero.',
kcalDelta: -80,
},
],
},
{
name: 'Branzino in crosta di erbe',
+6 -1
View File
@@ -1,7 +1,7 @@
import { PrismaClient } from '@prisma/client';
import { SEED_INGREDIENTS } from './seed-ingredients.js';
import { SEED_RECIPES } from './seed-recipes.js';
import { DEFAULT_SUBSTITUTES_BY_INGREDIENT, SEED_RECIPES } from './seed-recipes.js';
const prisma = new PrismaClient();
@@ -40,6 +40,7 @@ async function main(): Promise<void> {
let linksCreated = 0;
for (const r of SEED_RECIPES) {
let recipe = await prisma.recipe.findFirst({ where: { name: r.name } });
const variantsJson = r.variants ?? [];
if (!recipe) {
recipe = await prisma.recipe.create({
data: {
@@ -54,6 +55,7 @@ async function main(): Promise<void> {
netCarbG: r.netCarbG,
phases: r.phases,
notesChef: r.notesChef ?? null,
variants: variantsJson,
},
});
recipesInserted++;
@@ -71,6 +73,7 @@ async function main(): Promise<void> {
netCarbG: r.netCarbG,
phases: r.phases,
notesChef: r.notesChef ?? null,
variants: variantsJson,
},
});
}
@@ -83,12 +86,14 @@ async function main(): Promise<void> {
console.warn(`[seed] ricetta "${r.name}": ingrediente "${ing.name}" non trovato`);
continue;
}
const substitutes = ing.substitutes ?? DEFAULT_SUBSTITUTES_BY_INGREDIENT[ing.name] ?? [];
await prisma.recipeIngredient.create({
data: {
recipeId: recipe.id,
ingredientId,
quantity: ing.quantity,
unit: ing.unit,
substitutes,
},
});
linksCreated++;