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
+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 }) => ({