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>
152 lines
3.5 KiB
Dart
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,
|
|
);
|
|
}
|
|
}
|