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:
129
client/lib/shared/models/home_summary.dart
Normal file
129
client/lib/shared/models/home_summary.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user