feat: implement Iteration 5 — home screen dashboard

Backend:
- internal/home: GET /home/summary endpoint
  - Today's meal plan from menu_plans/menu_items
  - Logged calories sum from meal_diary
  - Daily goal from user profile (default 2000)
  - Expiring products within 3 days
  - Last 3 saved recommendations (no AI call on home load)
- Wire homeHandler in server.go and main.go

Flutter:
- shared/models/home_summary.dart: HomeSummary, TodaySummary,
  TodayMealPlan, ExpiringSoon, HomeRecipe
- features/home/home_service.dart + home_provider.dart
- features/home/home_screen.dart: greeting, calorie progress bar,
  today's meals card, expiring banner, quick actions row,
  recommendations horizontal list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-02-22 15:25:28 +02:00
parent d53e019d90
commit 9530dc6ff9
8 changed files with 899 additions and 4 deletions

View File

@@ -0,0 +1,129 @@
class HomeSummary {
final TodaySummary today;
final List<ExpiringSoon> expiringSoon;
final List<HomeRecipe> recommendations;
const HomeSummary({
required this.today,
required this.expiringSoon,
required this.recommendations,
});
factory HomeSummary.fromJson(Map<String, dynamic> json) => HomeSummary(
today: TodaySummary.fromJson(json['today'] as Map<String, dynamic>),
expiringSoon: (json['expiring_soon'] as List<dynamic>? ?? [])
.map((e) => ExpiringSoon.fromJson(e as Map<String, dynamic>))
.toList(),
recommendations: (json['recommendations'] as List<dynamic>? ?? [])
.map((e) => HomeRecipe.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
class TodaySummary {
final String date;
final int dailyGoal;
final double loggedCalories;
final List<TodayMealPlan> plan;
const TodaySummary({
required this.date,
required this.dailyGoal,
required this.loggedCalories,
required this.plan,
});
factory TodaySummary.fromJson(Map<String, dynamic> json) => TodaySummary(
date: json['date'] as String,
dailyGoal: (json['daily_goal'] as num).toInt(),
loggedCalories: (json['logged_calories'] as num).toDouble(),
plan: (json['plan'] as List<dynamic>? ?? [])
.map((e) => TodayMealPlan.fromJson(e as Map<String, dynamic>))
.toList(),
);
double get remainingCalories =>
dailyGoal > 0 ? (dailyGoal - loggedCalories).clamp(0, dailyGoal.toDouble()) : 0;
double get progress =>
dailyGoal > 0 ? (loggedCalories / dailyGoal).clamp(0.0, 1.0) : 0;
}
class TodayMealPlan {
final String mealType;
final String? recipeTitle;
final String? recipeImageUrl;
final double? calories;
const TodayMealPlan({
required this.mealType,
this.recipeTitle,
this.recipeImageUrl,
this.calories,
});
factory TodayMealPlan.fromJson(Map<String, dynamic> json) => TodayMealPlan(
mealType: json['meal_type'] as String,
recipeTitle: json['recipe_title'] as String?,
recipeImageUrl: json['recipe_image_url'] as String?,
calories: (json['calories'] as num?)?.toDouble(),
);
bool get hasRecipe => recipeTitle != null;
String get mealLabel => switch (mealType) {
'breakfast' => 'Завтрак',
'lunch' => 'Обед',
'dinner' => 'Ужин',
_ => mealType,
};
String get mealEmoji => switch (mealType) {
'breakfast' => '🌅',
'lunch' => '☀️',
'dinner' => '🌙',
_ => '🍽️',
};
}
class ExpiringSoon {
final String name;
final int expiresInDays;
final String quantity;
const ExpiringSoon({
required this.name,
required this.expiresInDays,
required this.quantity,
});
factory ExpiringSoon.fromJson(Map<String, dynamic> json) => ExpiringSoon(
name: json['name'] as String,
expiresInDays: (json['expires_in_days'] as num).toInt(),
quantity: json['quantity'] as String,
);
String get expiryLabel =>
expiresInDays == 0 ? 'сегодня' : 'через $expiresInDays д.';
}
class HomeRecipe {
final String id;
final String title;
final String imageUrl;
final double? calories;
const HomeRecipe({
required this.id,
required this.title,
required this.imageUrl,
this.calories,
});
factory HomeRecipe.fromJson(Map<String, dynamic> json) => HomeRecipe(
id: json['id'] as String,
title: json['title'] as String,
imageUrl: json['image_url'] as String? ?? '',
calories: (json['calories'] as num?)?.toDouble(),
);
}