feat: add batch-cooking sessions (single "mega recipe" with dish sections)

New AI-generated batch-cooking flow: pick N dinners/lunches + servings,
get one unified recipe covering a full prep session — merged shopping
list, steps tagged per-dish (a step can advance several dishes at once,
e.g. a shared oven bake), and per-dish storage (fridge/freezer) + exact
day-of reheat/finishing instructions.

Schema: recipes.isBatchCook, recipeSteps.appliesTo (dish names), new
recipeBatchDishes table. Batch recipes are excluded from the normal
/recipes list and live under their own /batch-cooking section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-12 01:23:37 +02:00
parent 68f0f490b9
commit 78d0599ffc
16 changed files with 5343 additions and 22 deletions
@@ -0,0 +1,17 @@
CREATE TABLE "recipe_batch_dishes" (
"id" text PRIMARY KEY NOT NULL,
"recipe_id" text NOT NULL,
"name" text NOT NULL,
"description" text,
"order" integer DEFAULT 0 NOT NULL,
"fridge_days" integer NOT NULL,
"freezer_friendly" boolean DEFAULT false NOT NULL,
"freezer_note" text,
"day_of_instructions" text NOT NULL
);
--> statement-breakpoint
ALTER TABLE "recipe_steps" ADD COLUMN "applies_to" text[] DEFAULT '{}' NOT NULL;--> statement-breakpoint
ALTER TABLE "recipes" ADD COLUMN "is_batch_cook" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "recipe_batch_dishes" ADD CONSTRAINT "recipe_batch_dishes_recipe_id_recipes_id_fk" FOREIGN KEY ("recipe_id") REFERENCES "public"."recipes"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "recipe_batch_dishes_recipe_idx" ON "recipe_batch_dishes" USING btree ("recipe_id");--> statement-breakpoint
CREATE INDEX "recipes_batch_cook_idx" ON "recipes" USING btree ("is_batch_cook");
File diff suppressed because it is too large Load Diff
@@ -204,6 +204,13 @@
"when": 1783671061049,
"tag": "0028_smiling_ezekiel_stane",
"breakpoints": true
},
{
"idx": 29,
"version": "7",
"when": 1783810665920,
"tag": "0029_tired_crystal",
"breakpoints": true
}
]
}
+22
View File
@@ -45,12 +45,14 @@ export const recipes = pgTable("recipes", {
prepMins: integer("prep_mins"),
cookMins: integer("cook_mins"),
tags: text("tags").array().notNull().default([]),
isBatchCook: boolean("is_batch_cook").notNull().default(false),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
}, (t) => [
index("recipes_author_idx").on(t.authorId),
index("recipes_visibility_idx").on(t.visibility),
index("recipes_dietary_tags_gin").using("gin", t.dietaryTags),
index("recipes_batch_cook_idx").on(t.isBatchCook),
]);
export const ingredients = pgTable("ingredients", {
@@ -82,10 +84,25 @@ export const recipeSteps = pgTable("recipe_steps", {
instruction: text("instruction").notNull(),
timerSeconds: integer("timer_seconds"),
photoUrl: text("photo_url"),
appliesTo: text("applies_to").array().notNull().default([]),
}, (t) => [
index("recipe_steps_recipe_idx").on(t.recipeId),
]);
export const recipeBatchDishes = pgTable("recipe_batch_dishes", {
id: text("id").primaryKey(),
recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }),
name: text("name").notNull(),
description: text("description"),
order: integer("order").notNull().default(0),
fridgeDays: integer("fridge_days").notNull(),
freezerFriendly: boolean("freezer_friendly").notNull().default(false),
freezerNote: text("freezer_note"),
dayOfInstructions: text("day_of_instructions").notNull(),
}, (t) => [
index("recipe_batch_dishes_recipe_idx").on(t.recipeId),
]);
export const recipePhotos = pgTable("recipe_photos", {
id: text("id").primaryKey(),
recipeId: text("recipe_id").notNull().references(() => recipes.id, { onDelete: "cascade" }),
@@ -128,6 +145,11 @@ export const recipesRelations = relations(recipes, ({ one, many }) => ({
notes: many(recipeNotes),
childVariations: many(recipeVariations, { relationName: "parent" }),
parentVariations: many(recipeVariations, { relationName: "child" }),
batchDishes: many(recipeBatchDishes),
}));
export const recipeBatchDishesRelations = relations(recipeBatchDishes, ({ one }) => ({
recipe: one(recipes, { fields: [recipeBatchDishes.recipeId], references: [recipes.id] }),
}));
export const recipeIngredientsRelations = relations(recipeIngredients, ({ one }) => ({