Files
food-ai/docs/plans/Iteration_5.md
dbastrikin d53e019d90 docs: add Iteration 5 — home screen dashboard
Describe GET /home/summary endpoint (plan, logged calories, expiring
products, cached recommendations) and HomeScreen layout with calorie
ring, today's meals, expiring banner, and quick actions.
Update Summary.md to include iteration 5 and fix provider references
from Gemini/Groq to OpenAI/GPT.

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

279 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Итерация 5: Главный экран
**Цель:** превратить заглушку главного экрана в информационный дашборд — показать сводку на сегодня (плановые приёмы пищи, прогресс калорий), быстрые действия, предупреждение об истекающих продуктах и рекомендации рецептов.
**Зависимости:** Итерации 1 (рецепты), 2 (продукты), 4 (меню, дневник).
---
## Структура задач
```
5.1 Backend: endpoint сводки
└── GET /home/summary
5.2 Flutter: HomeScreen
├── 5.2.1 Приветствие + дата
├── 5.2.2 Прогресс калорий (ring)
├── 5.2.3 Плановые приёмы пищи на сегодня
├── 5.2.4 Строка быстрых действий
├── 5.2.5 Баннер истекающих продуктов
└── 5.2.6 Блок рекомендаций
5.3 Flutter: homeProvider
```
---
## 5.1 Backend: GET /home/summary
Единственный запрос при загрузке главного экрана. Агрегирует данные из нескольких таблиц — без AI-вызовов, без медленных операций.
### Запрос
```
GET /home/summary
Authorization: Bearer <jwt>
```
### Ответ
```json
{
"today": {
"date": "2026-02-22",
"daily_goal": 2000,
"logged_calories": 650,
"plan": [
{
"meal_type": "breakfast",
"recipe_title": "Овсяная каша с яблоком",
"recipe_image_url": "https://...",
"calories": 320
},
{
"meal_type": "lunch",
"recipe_title": "Куриный суп",
"recipe_image_url": "https://...",
"calories": 480
},
{
"meal_type": "dinner",
"recipe_title": null,
"recipe_image_url": null,
"calories": null
}
]
},
"expiring_soon": [
{ "name": "Куриная грудка", "expires_in_days": 1, "quantity": "300 г" },
{ "name": "Молоко", "expires_in_days": 2, "quantity": "0.5 л" }
],
"recommendations": [
{
"id": "uuid",
"title": "Стир-фрай с курицей",
"image_url": "https://...",
"calories": 510
},
{
"id": "uuid",
"title": "Омлет с овощами",
"image_url": "https://...",
"calories": 380
}
]
}
```
### Логика сборки
```
today.plan:
SELECT mi.meal_type, sr.title, sr.image_url, sr.nutrition->>'calories'
FROM menu_plans mp
JOIN menu_items mi ON mi.menu_plan_id = mp.id
LEFT JOIN saved_recipes sr ON sr.id = mi.recipe_id
WHERE mp.user_id = $1
AND mp.week_start::text = $2 -- понедельник текущей недели
AND mi.day_of_week = $3 -- номер дня (17)
ORDER BY CASE mi.meal_type WHEN 'breakfast' THEN 1
WHEN 'lunch' THEN 2
ELSE 3 END
Если план не существует — вернуть три слота с null.
today.logged_calories:
SELECT COALESCE(SUM(calories * portions), 0)
FROM meal_diary
WHERE user_id = $1 AND date = $2
today.daily_goal:
users.daily_calories (или 2000 по умолчанию)
expiring_soon:
SELECT name, quantity, unit,
EXTRACT(EPOCH FROM (expires_at - now())) / 86400 AS expires_in_days
FROM products
WHERE user_id = $1
AND expires_at IS NOT NULL
AND expires_at <= now() + INTERVAL '3 days'
AND expires_at >= now()
ORDER BY expires_at
LIMIT 5
recommendations:
SELECT id, title, image_url, nutrition->>'calories' AS calories
FROM saved_recipes
WHERE user_id = $1 AND source = 'recommendation'
ORDER BY saved_at DESC
LIMIT 3
```
Рекомендации берутся из последних результатов `GET /recommendations` (сохранённых в `saved_recipes` с `source='recommendation'`). Новый AI-вызов не производится — главный экран не тратит AI-квоту.
---
## 5.2 Flutter: HomeScreen
### Макет
```
┌─────────────────────────────────────┐
│ Доброе утро, Алексей! 22 февраля │
├─────────────────────────────────────┤
│ │
│ Калории сегодня │
│ ┌───────┐ │
│ │ 650 │ 650 / 2000 ккал │
│ │ ккал │ ▓▓▓▓░░░░░░░░░░ │
│ └───────┘ осталось 1350 │
│ │
├─────────────────────────────────────┤
│ Приёмы пищи сегодня │
│ ┌────────────────────────────────┐ │
│ │ 🌅 Завтрак Овсянка с яблоком │ │
│ │ 320 ккал ✓ │ │
│ ├────────────────────────────────┤ │
│ │ ☀ Обед Куриный суп │ │
│ │ 480 ккал ✓ │ │
│ ├────────────────────────────────┤ │
│ │ 🌙 Ужин Не запланировано │ │
│ │ → │ │
│ └────────────────────────────────┘ │
│ │
├─────────────────────────────────────┤
│ ⚠ Истекает срок │
│ Куриная грудка — 1 день │
│ Молоко — 2 дня → │
├─────────────────────────────────────┤
│ Быстрые действия │
│ [📷 Сканировать] [✨ Меню] [📖 Дневник] │
├─────────────────────────────────────┤
│ Рекомендуем приготовить │
│ ┌──────────┐ ┌──────────┐ │
│ │ [фото] │ │ [фото] │ │
│ │ Стир-фрай│ │ Омлет │ │
│ │ 510 ккал │ │ 380 ккал │ │
│ └──────────┘ └──────────┘ │
│ │
├─────────────────────────────────────┤
│ [Главная●] [Продукты] [Меню] [Рецепты] [Профиль] │
└─────────────────────────────────────┘
```
### 5.2.1 Приветствие
Время-зависимое:
- до 12:00 — «Доброе утро»
- до 18:00 — «Добрый день»
- до 24:00 — «Добрый вечер»
Имя берётся из профиля пользователя.
### 5.2.2 Прогресс калорий
Кольцевой индикатор (`CustomPaint` или пакет `percent_indicator`):
- Заполнен на `logged / goal * 100%`
- Центр: «N ккал»
- Подпись: «из N ккал», «осталось N ккал»
- Если цель не задана (daily_goal = 0) — скрыть блок
### 5.2.3 Плановые приёмы пищи
Карточка с тремя строками (завтрак / обед / ужин).
- Если рецепт есть — показать название + калории
- Если рецепта нет — «Не запланировано» + стрелка → `/menu`
- Тап по строке с рецептом → `RecipeDetailScreen`
### 5.2.4 Быстрые действия
Три кнопки в строку:
- **Сканировать** → `/scan`
- **Меню** → `/menu`
- **Дневник** → `/menu/diary?date=today`
### 5.2.5 Баннер истекающих продуктов
Показывается только если `expiring_soon` не пуст.
- Жёлтый/оранжевый фон
- Список: «Куриная грудка — 1 день», «Молоко — 2 дня»
- Тап → `/products` (с фильтром «Истекает»)
### 5.2.6 Рекомендации
Горизонтальный `ListView` с карточками:
- Фото (CachedNetworkImage)
- Название
- «≈N ккал»
- Тап → `RecipeDetailScreen`
Если `recommendations` пуст — показать кнопку «Получить рекомендации» → `/recipes`.
---
## 5.3 Flutter: homeProvider
```dart
// Модель
class HomeSummary {
final TodaySummary today;
final List<ExpiringSoon> expiringSoon;
final List<HomeRecipe> recommendations;
}
// Провайдер
final homeProvider = StateNotifierProvider
<HomeNotifier, AsyncValue<HomeSummary>>((ref) {
return HomeNotifier(ref.read(homeServiceProvider));
});
class HomeNotifier extends StateNotifier<AsyncValue<HomeSummary>> {
HomeNotifier(this._service) : super(const AsyncValue.loading()) {
load();
}
Future<void> load() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => _service.getSummary());
}
}
```
Провайдер обновляется:
- При первом открытии вкладки
- Через `ref.invalidate(homeProvider)` после сканирования/добавления продуктов или генерации меню
---
## Оценка нагрузки
| Действие | OpenAI | Pexels | БД запросов |
|---|---|---|---|
| Открытие главного экрана | 0 | 0 | 4 (plan, diary, expiring, recommendations) |
| Рекомендации (первый раз) | через `/recommendations` | — | — |
Главный экран не тратит AI-квоту. Рекомендации показываются из уже сохранённых рецептов.