Files
food-ai/client/lib/shared/models/menu.dart
dbastrikin ea8e207a45 feat: implement Iteration 4 — menu planning, shopping list, diary
Backend:
- Migrations 007 (menu_plans, menu_items, shopping_lists) and
  008 (meal_diary)
- gemini/menu.go: GenerateMenu — 7-day × 3-meal plan via one Groq call
- internal/menu: model, repository (GetByWeek, SaveMenuInTx,
  shopping list CRUD), handler (GET/PUT/DELETE /menu,
  POST /ai/generate-menu, shopping list endpoints)
- internal/diary: model, repository, handler (GET/POST/DELETE /diary)
- Increase server WriteTimeout to 120s for long AI calls
- api_client.go: add patch() and postList() helpers

Flutter:
- shared/models: menu.dart, shopping_item.dart, diary_entry.dart
- features/menu: menu_service.dart, menu_provider.dart
  (MenuNotifier, ShoppingListNotifier, DiaryNotifier with family)
- MenuScreen: 7-day view, week nav, skeleton on generation,
  generate FAB with confirmation dialog
- ShoppingListScreen: items by category, optimistic checkbox toggle
- DiaryScreen: daily entries with swipe-to-delete, add-entry sheet
- Router: /menu/shopping-list and /menu/diary routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 12:00:25 +02:00

152 lines
3.5 KiB
Dart

import 'recipe.dart';
class MenuPlan {
final String id;
final String weekStart;
final List<MenuDay> days;
const MenuPlan({
required this.id,
required this.weekStart,
required this.days,
});
factory MenuPlan.fromJson(Map<String, dynamic> json) {
return MenuPlan(
id: json['id'] as String? ?? '',
weekStart: json['week_start'] as String? ?? '',
days: (json['days'] as List<dynamic>? ?? [])
.map((e) => MenuDay.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
}
class MenuDay {
final int day;
final String date;
final List<MealSlot> meals;
final double totalCalories;
const MenuDay({
required this.day,
required this.date,
required this.meals,
required this.totalCalories,
});
factory MenuDay.fromJson(Map<String, dynamic> json) {
return MenuDay(
day: json['day'] as int? ?? 0,
date: json['date'] as String? ?? '',
meals: (json['meals'] as List<dynamic>? ?? [])
.map((e) => MealSlot.fromJson(e as Map<String, dynamic>))
.toList(),
totalCalories: (json['total_calories'] as num?)?.toDouble() ?? 0,
);
}
// Localized day name.
String get dayName {
const names = [
'Понедельник',
'Вторник',
'Среда',
'Четверг',
'Пятница',
'Суббота',
'Воскресенье',
];
final i = day - 1;
return (i >= 0 && i < names.length) ? names[i] : 'День $day';
}
// Short date label like "16 фев".
String get shortDate {
try {
final dt = DateTime.parse(date);
const months = [
'янв', 'фев', 'мар', 'апр', 'май', 'июн',
'июл', 'авг', 'сен', 'окт', 'ноя', 'дек',
];
return '${dt.day} ${months[dt.month - 1]}';
} catch (_) {
return date;
}
}
}
class MealSlot {
final String id;
final String mealType;
final MenuRecipe? recipe;
const MealSlot({
required this.id,
required this.mealType,
this.recipe,
});
factory MealSlot.fromJson(Map<String, dynamic> json) {
return MealSlot(
id: json['id'] as String? ?? '',
mealType: json['meal_type'] as String? ?? '',
recipe: json['recipe'] != null
? MenuRecipe.fromJson(json['recipe'] as Map<String, dynamic>)
: null,
);
}
String get mealLabel {
switch (mealType) {
case 'breakfast':
return 'Завтрак';
case 'lunch':
return 'Обед';
case 'dinner':
return 'Ужин';
default:
return mealType;
}
}
String get mealEmoji {
switch (mealType) {
case 'breakfast':
return '🌅';
case 'lunch':
return '☀️';
case 'dinner':
return '🌙';
default:
return '🍽️';
}
}
}
class MenuRecipe {
final String id;
final String title;
final String imageUrl;
final NutritionInfo? nutrition;
const MenuRecipe({
required this.id,
required this.title,
required this.imageUrl,
this.nutrition,
});
factory MenuRecipe.fromJson(Map<String, dynamic> json) {
return MenuRecipe(
id: json['id'] as String? ?? '',
title: json['title'] as String? ?? '',
imageUrl: json['image_url'] as String? ?? '',
nutrition: json['nutrition_per_serving'] != null
? NutritionInfo.fromJson(
json['nutrition_per_serving'] as Map<String, dynamic>)
: null,
);
}
}