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>
This commit is contained in:
278
docs/plans/Iteration_5.md
Normal file
278
docs/plans/Iteration_5.md
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
# Итерация 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 -- номер дня (1–7)
|
||||||
|
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-квоту. Рекомендации показываются из уже сохранённых рецептов.
|
||||||
@@ -5,10 +5,11 @@
|
|||||||
| # | Итерация | Цель | Зависит от |
|
| # | Итерация | Цель | Зависит от |
|
||||||
|---|----------|------|------------|
|
|---|----------|------|------------|
|
||||||
| 0 | Фундамент | Go-проект, БД, авторизация, Flutter-каркас | — |
|
| 0 | Фундамент | Go-проект, БД, авторизация, Flutter-каркас | — |
|
||||||
| 1 | AI-рекомендации рецептов | Gemini генерирует рецепты, Pexels фото, сохранение рецептов | 0 |
|
| 1 | AI-рекомендации рецептов | GPT генерирует рецепты, Pexels фото, сохранение рецептов | 0 |
|
||||||
| 2 | Управление продуктами | CRUD продуктов, сроки хранения, ingredient_mappings | 0 |
|
| 2 | Управление продуктами | CRUD продуктов, сроки хранения, ingredient_mappings | 0 |
|
||||||
| 3 | Распознавание продуктов | OCR чека, фото продуктов, фото блюд (Gemini Vision) | 1, 2 |
|
| 3 | Распознавание продуктов | OCR чека, фото продуктов, фото блюд (GPT-4o Vision) | 1, 2 |
|
||||||
| 4 | Планирование меню | Меню на неделю, AI-генерация, список покупок, дневник | 1, 2 |
|
| 4 | Планирование меню | Меню на неделю, AI-генерация, список покупок, дневник | 1, 2 |
|
||||||
|
| 5 | Главный экран | Дашборд: калории, план на сегодня, истекающие продукты, рекомендации | 1, 2, 4 |
|
||||||
|
|
||||||
Дальнейшие итерации определяются приоритетами после MVP. Функциональность из TODO.md (дневник статистики, режим готовки, полировка) — следующий горизонт.
|
Дальнейшие итерации определяются приоритетами после MVP. Функциональность из TODO.md (дневник статистики, режим готовки, полировка) — следующий горизонт.
|
||||||
|
|
||||||
@@ -36,11 +37,17 @@
|
|||||||
┌────────────────────┐ ┌─────────────────────┐
|
┌────────────────────┐ ┌─────────────────────┐
|
||||||
│ 3. Распознавание │ │ 4. Планирование │
|
│ 3. Распознавание │ │ 4. Планирование │
|
||||||
│ продуктов │ │ меню │
|
│ продуктов │ │ меню │
|
||||||
│ (Gemini Vision) │ │ (Gemini+Pexels) │
|
│ (GPT-4o Vision) │ │ (GPT+Pexels) │
|
||||||
└────────────────────┘ └─────────────────────┘
|
└────────────────────┘ └──────────┬──────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ 5. Главный экран │
|
||||||
|
│ (дашборд) │
|
||||||
|
└─────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
**Параллельная разработка:** итерации 1 и 2 могут выполняться параллельно. Итерации 3 и 4 — тоже параллельно после завершения 1 и 2.
|
**Параллельная разработка:** итерации 1 и 2 могут выполняться параллельно. Итерации 3 и 4 — тоже параллельно после завершения 1 и 2. Итерация 5 — после 4.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -208,9 +215,9 @@
|
|||||||
|----|-------|----------|
|
|----|-------|----------|
|
||||||
| 4.1 | Таблицы menu_plans, menu_items | Миграции. menu_items → saved_recipes |
|
| 4.1 | Таблицы menu_plans, menu_items | Миграции. menu_items → saved_recipes |
|
||||||
| 4.2 | Таблица meal_diary | Миграция. Записи приёмов пищи |
|
| 4.2 | Таблица meal_diary | Миграция. Записи приёмов пищи |
|
||||||
| 4.3 | POST /ai/generate-menu | Gemini генерирует 21 рецепт, Pexels параллельно, сохранение в БД |
|
| 4.3 | POST /ai/generate-menu | GPT генерирует 21 рецепт, Pexels параллельно, сохранение в БД |
|
||||||
| 4.4 | Menu CRUD | GET /menu?week=, PUT /menu/items/{id}, DELETE /menu/items/{id} |
|
| 4.4 | Menu CRUD | GET /menu?week=, PUT /menu/items/{id}, DELETE /menu/items/{id} |
|
||||||
| 4.5 | Shopping list | POST /shopping-list/generate (SQL-агрегация без Gemini), GET, PATCH check |
|
| 4.5 | Shopping list | POST /shopping-list/generate (SQL-агрегация без AI), GET, PATCH check |
|
||||||
|
|
||||||
#### Flutter
|
#### Flutter
|
||||||
|
|
||||||
@@ -222,22 +229,53 @@
|
|||||||
| 4.9 | DiaryScreen | Записи за день, «+ Добавить» |
|
| 4.9 | DiaryScreen | Записи за день, «+ Добавить» |
|
||||||
|
|
||||||
### Результат итерации
|
### Результат итерации
|
||||||
- Пользователь получает меню на неделю одним запросом к Gemini
|
- Пользователь получает меню на неделю одним запросом к GPT
|
||||||
- Все рецепты меню сохраняются в saved_recipes
|
- Все рецепты меню сохраняются в saved_recipes
|
||||||
- Из меню автоматически формируется список покупок (то, чего нет в запасах)
|
- Из меню автоматически формируется список покупок (то, чего нет в запасах)
|
||||||
- Ведётся дневник питания
|
- Ведётся дневник питания
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Итерация 5: Главный экран
|
||||||
|
|
||||||
|
> **Детальный план:** [Iteration_5.md](./Iteration_5.md)
|
||||||
|
|
||||||
|
**Цель:** дашборд — сводка на сегодня, прогресс калорий, плановые приёмы пищи, предупреждение об истекающих продуктах, рекомендации рецептов.
|
||||||
|
|
||||||
|
**Зависимости:** итерации 1, 2, 4.
|
||||||
|
|
||||||
|
### User Stories
|
||||||
|
|
||||||
|
#### Backend
|
||||||
|
|
||||||
|
| ID | Story | Описание |
|
||||||
|
|----|-------|----------|
|
||||||
|
| 5.1 | GET /home/summary | Агрегирует: план на сегодня, калории дневника, истекающие продукты, последние рекомендации |
|
||||||
|
|
||||||
|
#### Flutter
|
||||||
|
|
||||||
|
| ID | Story | Описание |
|
||||||
|
|----|-------|----------|
|
||||||
|
| 5.2 | HomeScreen | Приветствие, кольцо калорий, план на сегодня, быстрые действия, баннер истекающих, блок рекомендаций |
|
||||||
|
| 5.3 | homeProvider | StateNotifier, обновление после scan/menu-генерации |
|
||||||
|
|
||||||
|
### Результат итерации
|
||||||
|
- Открываешь приложение — сразу видишь баланс калорий и план на день
|
||||||
|
- Предупреждение если продукты скоро истекут
|
||||||
|
- Рекомендации без нового AI-вызова (из ранее сохранённых)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Итоги
|
## Итоги
|
||||||
|
|
||||||
| Итерация | Цель | Ключевые API |
|
| Итерация | Цель | Ключевые API |
|
||||||
|----------|------|-------------|
|
|----------|------|-------------|
|
||||||
| 0. Фундамент | Auth, профиль, каркас | Firebase |
|
| 0. Фундамент | Auth, профиль, каркас | Firebase |
|
||||||
| 1. AI-рекомендации | Рецепты + сохранение | Gemini, Pexels |
|
| 1. AI-рекомендации | Рецепты + сохранение | GPT-4o-mini, Pexels |
|
||||||
| 2. Продукты | CRUD запасов, ingredient_mappings | — |
|
| 2. Продукты | CRUD запасов, ingredient_mappings | — |
|
||||||
| 3. Распознавание | OCR чека, фото продуктов/блюда | Gemini Vision |
|
| 3. Распознавание | OCR чека, фото продуктов/блюда | GPT-4o Vision |
|
||||||
| 4. Меню | Недельное меню, список покупок | Gemini, Pexels |
|
| 4. Меню | Недельное меню, список покупок | GPT-4o-mini, Pexels |
|
||||||
|
| 5. Главный экран | Дашборд: калории, план, истекающие, рекомендации | — |
|
||||||
|
|
||||||
**MVP:** итерации 0–2 (авторизация + рекомендации + продукты) — пользователь получает персонализированные рецепты.
|
**MVP:** итерации 0–2 (авторизация + рекомендации + продукты) — пользователь получает персонализированные рецепты.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user