270 lines
9.5 KiB
Markdown
270 lines
9.5 KiB
Markdown
|
|
# Phase 8 — Meal Planning & Waste Reduction
|
|||
|
|
|
|||
|
|
**Goal**: Enable weekly meal planning with automatic nutrition tracking vs targets. The core feature is a **suggestion engine** that recommends recipes prioritizing ingredients already in the pantry (especially those expiring soon), reducing food waste while maintaining nutritional balance.
|
|||
|
|
|
|||
|
|
**Depends on**: Phase 5 (products), Phase 6 (recipes), Phase 7 (pantry)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Deliverables
|
|||
|
|
|
|||
|
|
1. `MealPlan` and `NutritionTarget` MongoDB schemas
|
|||
|
|
2. Meal plan CRUD API with daily/weekly views
|
|||
|
|
3. Recipe suggestion engine (algorithmic, not LLM-dependent)
|
|||
|
|
4. Nutrition targets per user with daily tracking
|
|||
|
|
5. Shopping gap analysis (what's needed beyond pantry)
|
|||
|
|
6. Meal plan web UI with weekly calendar and suggestion panel
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Data Model
|
|||
|
|
|
|||
|
|
### MealPlan Schema
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// packages/shared/src/types/meal-plan.ts
|
|||
|
|
export interface MealPlan {
|
|||
|
|
id: string;
|
|||
|
|
householdId: string;
|
|||
|
|
weekStartDate: Date; // Monday of the planning week
|
|||
|
|
days: MealPlanDay[];
|
|||
|
|
status: MealPlanStatus;
|
|||
|
|
shoppingListId?: string; // Auto-generated shopping list (Phase 9 link)
|
|||
|
|
createdBy: string;
|
|||
|
|
createdAt: Date;
|
|||
|
|
updatedAt: Date;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface MealPlanDay {
|
|||
|
|
date: Date;
|
|||
|
|
meals: PlannedMeal[];
|
|||
|
|
dailyNutritionTotal: NutritionInfo; // Computed
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface PlannedMeal {
|
|||
|
|
id: string; // UUID for drag-and-drop reference
|
|||
|
|
type: MealType;
|
|||
|
|
recipeId?: string; // Linked recipe
|
|||
|
|
recipeName: string; // Denormalized
|
|||
|
|
servings: number;
|
|||
|
|
customName?: string; // For non-recipe meals
|
|||
|
|
customNutrition?: NutritionInfo; // Manual override for non-recipe meals
|
|||
|
|
perServingNutrition: NutritionInfo; // From recipe or custom
|
|||
|
|
notes?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export enum MealType {
|
|||
|
|
BREAKFAST = 'breakfast',
|
|||
|
|
LUNCH = 'lunch',
|
|||
|
|
DINNER = 'dinner',
|
|||
|
|
SNACK = 'snack',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export enum MealPlanStatus {
|
|||
|
|
DRAFT = 'draft',
|
|||
|
|
ACTIVE = 'active',
|
|||
|
|
COMPLETED = 'completed',
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### NutritionTarget Schema
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// packages/shared/src/types/nutrition-target.ts
|
|||
|
|
export interface NutritionTarget {
|
|||
|
|
id: string;
|
|||
|
|
userId: string;
|
|||
|
|
householdId: string;
|
|||
|
|
dailyCalories: number;
|
|||
|
|
proteinG: number;
|
|||
|
|
carbsG: number;
|
|||
|
|
fatG: number;
|
|||
|
|
fiberG?: number;
|
|||
|
|
sodiumMg?: number;
|
|||
|
|
sugarG?: number;
|
|||
|
|
isActive: boolean;
|
|||
|
|
createdAt: Date;
|
|||
|
|
updatedAt: Date;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### MongoDB Indexes
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
{ householdId: 1, weekStartDate: 1 } // unique per household per week
|
|||
|
|
{ householdId: 1, status: 1 }
|
|||
|
|
{ householdId: 1, 'days.meals.recipeId': 1 } // Find plans using a recipe
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## API Endpoints
|
|||
|
|
|
|||
|
|
### MealPlanModule
|
|||
|
|
|
|||
|
|
| Method | Path | Description | Auth |
|
|||
|
|
| ------ | ------------------------------ | ---------------------------------------------- | ------ |
|
|||
|
|
| GET | `/meal-plans` | List meal plans (paginated) | member |
|
|||
|
|
| GET | `/meal-plans/current` | Get current week's plan | member |
|
|||
|
|
| GET | `/meal-plans/:id` | Get specific plan | member |
|
|||
|
|
| POST | `/meal-plans` | Create new week plan | member |
|
|||
|
|
| PATCH | `/meal-plans/:id` | Update plan (add/move/remove meals) | member |
|
|||
|
|
| DELETE | `/meal-plans/:id` | Delete plan (draft only) | admin |
|
|||
|
|
| POST | `/meal-plans/:id/activate` | Set plan as active | member |
|
|||
|
|
| GET | `/meal-plans/:id/shopping-gap` | What's needed beyond current pantry | member |
|
|||
|
|
| GET | `/meal-plans/suggestions` | Get recipe suggestions for current pantry | member |
|
|||
|
|
| POST | `/meal-plans/suggest-with-llm` | LLM-powered meal plan generation (placeholder) | member |
|
|||
|
|
|
|||
|
|
### NutritionTargetModule
|
|||
|
|
|
|||
|
|
| Method | Path | Description | Auth |
|
|||
|
|
| ------ | ------------------------ | -------------------------------- | ------ |
|
|||
|
|
| GET | `/nutrition-targets` | Get current user's active target | member |
|
|||
|
|
| POST | `/nutrition-targets` | Set nutrition targets | member |
|
|||
|
|
| PATCH | `/nutrition-targets/:id` | Update targets | member |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Tasks
|
|||
|
|
|
|||
|
|
### 8.1 — Shared Types & Validation
|
|||
|
|
|
|||
|
|
- Add all types above to `packages/shared`
|
|||
|
|
- Zod schemas for create/update operations
|
|||
|
|
- Meal plan day validation (7 days per plan, valid dates)
|
|||
|
|
|
|||
|
|
### 8.2 — Meal Plan CRUD
|
|||
|
|
|
|||
|
|
- Standard CRUD with computed `dailyNutritionTotal` per day:
|
|||
|
|
- Sum `perServingNutrition * servings` for all meals in each day
|
|||
|
|
- On meal plan creation: default to 7 empty days (Monday–Sunday)
|
|||
|
|
- On update: support granular operations:
|
|||
|
|
- `addMeal(dayIndex, meal)`
|
|||
|
|
- `removeMeal(dayIndex, mealId)`
|
|||
|
|
- `moveMeal(fromDay, toDay, mealId)` — for drag-and-drop
|
|||
|
|
- `updateMeal(dayIndex, mealId, updates)`
|
|||
|
|
|
|||
|
|
### 8.3 — Recipe Suggestion Engine (Core Algorithm)
|
|||
|
|
|
|||
|
|
This is the **key differentiating feature** — algorithmic, no LLM required.
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
class RecipeSuggestionService {
|
|||
|
|
/**
|
|||
|
|
* Score and rank recipes based on pantry state and user preferences.
|
|||
|
|
*
|
|||
|
|
* Input:
|
|||
|
|
* - Current pantry items (with freshness urgency)
|
|||
|
|
* - Recipe catalog for the household
|
|||
|
|
* - User's nutrition targets (optional)
|
|||
|
|
* - Already planned meals this week (to avoid repetition)
|
|||
|
|
*
|
|||
|
|
* Scoring per recipe:
|
|||
|
|
* ingredientCoverageScore (0-40 pts): % of ingredients available in pantry
|
|||
|
|
* freshnessUrgencyScore (0-30 pts): bonus for using urgent/use-soon items
|
|||
|
|
* nutritionBalanceScore (0-20 pts): how well it complements the day's existing meals vs targets
|
|||
|
|
* varietyScore (0-10 pts): penalty for recently planned recipes
|
|||
|
|
*
|
|||
|
|
* Output per suggestion:
|
|||
|
|
* - recipe (id, name, perServingNutrition)
|
|||
|
|
* - score (total)
|
|||
|
|
* - availableIngredients[]: items from pantry that match
|
|||
|
|
* - missingIngredients[]: items not in pantry (with estimated cost from Phase 9 if available)
|
|||
|
|
* - urgentIngredients[]: pantry items with urgency=urgent that this recipe would use
|
|||
|
|
* - reasoning: human-readable explanation of why this recipe was suggested
|
|||
|
|
*/
|
|||
|
|
suggestRecipes(context: SuggestionContext): Promise<RecipeSuggestion[]>;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Ingredient matching logic**:
|
|||
|
|
|
|||
|
|
- Match recipe ingredient's `productId` against pantry items with `status: sealed|opened`
|
|||
|
|
- Check quantity: is there enough? (approximate — compare units, flag if unclear)
|
|||
|
|
- Prefer items with higher freshness urgency
|
|||
|
|
|
|||
|
|
**Scoring weights** (configurable per household):
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const DEFAULT_WEIGHTS = {
|
|||
|
|
ingredientCoverage: 40,
|
|||
|
|
freshnessUrgency: 30,
|
|||
|
|
nutritionBalance: 20,
|
|||
|
|
variety: 10,
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 8.4 — Shopping Gap Analysis
|
|||
|
|
|
|||
|
|
- `GET /meal-plans/:id/shopping-gap`:
|
|||
|
|
- For each recipe in the meal plan, list required ingredients
|
|||
|
|
- Cross-reference with current pantry (available quantity vs needed quantity)
|
|||
|
|
- Return:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface ShoppingGap {
|
|||
|
|
coveredByPantry: ShoppingGapItem[]; // Already have enough
|
|||
|
|
needToBuy: ShoppingGapItem[]; // Partially or fully missing
|
|||
|
|
pantryItemsUsed: PantryItemUsage[]; // Which pantry items will be consumed
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface ShoppingGapItem {
|
|||
|
|
productId: string;
|
|||
|
|
productName: string;
|
|||
|
|
totalNeeded: { quantity: number; unit: ServingUnit };
|
|||
|
|
availableInPantry: { quantity: number; unit: ServingUnit };
|
|||
|
|
shortfall: { quantity: number; unit: ServingUnit };
|
|||
|
|
usedInRecipes: string[]; // Recipe names
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- This output feeds directly into Phase 9's auto-generated shopping lists
|
|||
|
|
|
|||
|
|
### 8.5 — LLM Suggestion Placeholder
|
|||
|
|
|
|||
|
|
- `POST /meal-plans/suggest-with-llm`:
|
|||
|
|
- Builds a context object: pantry summary, nutrition targets, dietary preferences
|
|||
|
|
- Calls `ILlmProvider.suggestMealPlan(context)`
|
|||
|
|
- With `NoOpLlmProvider`: returns `{ available: false }`
|
|||
|
|
- When wired (Phase 10): returns a full week meal plan draft
|
|||
|
|
|
|||
|
|
### 8.6 — Web UI: Meal Planning
|
|||
|
|
|
|||
|
|
- `/meal-plans` page:
|
|||
|
|
- **Weekly calendar grid**: 7 columns (Mon–Sun) × 4 rows (Breakfast, Lunch, Dinner, Snack)
|
|||
|
|
- Each cell: drop zone for recipes, shows meal name + calorie badge
|
|||
|
|
- **Drag-and-drop**: drag recipes from suggestion panel or between cells
|
|||
|
|
- **Daily nutrition summary row** at bottom: calories, protein, carbs, fat bars
|
|||
|
|
- Color-coded vs user's nutrition targets (under = blue, on-target = green, over = red)
|
|||
|
|
- **Week navigation**: previous/next week arrows
|
|||
|
|
- **Suggestion panel** (sidebar or drawer):
|
|||
|
|
- "Suggestions based on your pantry" — ranked list from suggestion engine
|
|||
|
|
- Each suggestion shows: recipe name, match score, "Uses: [urgent items]", "Need to buy: [missing items]"
|
|||
|
|
- Click to expand: full ingredient match breakdown
|
|||
|
|
- "Add to plan" button → pick day + meal type
|
|||
|
|
- **Shopping gap tab**: shows what's needed beyond pantry, "Generate shopping list" button (Phase 9 integration)
|
|||
|
|
- `/nutrition-targets` settings:
|
|||
|
|
- Daily macro targets form (calories, protein, carbs, fat)
|
|||
|
|
- Preset templates: "Maintenance", "Weight loss", "Muscle gain", "Custom"
|
|||
|
|
- Visual preview: donut chart of macro ratios
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Acceptance Criteria
|
|||
|
|
|
|||
|
|
- [ ] Can create a weekly meal plan and add meals to specific days/slots
|
|||
|
|
- [ ] Daily nutrition totals are computed and displayed
|
|||
|
|
- [ ] Suggestion engine returns ranked recipes based on pantry state
|
|||
|
|
- [ ] Suggestions prioritize recipes using soon-to-expire pantry items
|
|||
|
|
- [ ] Shopping gap analysis correctly identifies missing ingredients
|
|||
|
|
- [ ] Drag-and-drop works in the weekly calendar UI
|
|||
|
|
- [ ] Nutrition targets can be set per user
|
|||
|
|
- [ ] Daily nutrition bars show progress vs targets
|
|||
|
|
- [ ] Variety scoring penalizes recently used recipes
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Estimated Effort
|
|||
|
|
|
|||
|
|
Large. The suggestion engine scoring algorithm, shopping gap analysis, and calendar UI with drag-and-drop are all significant features.
|