diff --git a/docs/plans/Iteration_5.md b/docs/plans/Iteration_5.md new file mode 100644 index 0000000..1d7aca4 --- /dev/null +++ b/docs/plans/Iteration_5.md @@ -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 +``` + +### Ответ + +```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; + final List recommendations; +} + +// Провайдер +final homeProvider = StateNotifierProvider + >((ref) { + return HomeNotifier(ref.read(homeServiceProvider)); +}); + +class HomeNotifier extends StateNotifier> { + HomeNotifier(this._service) : super(const AsyncValue.loading()) { + load(); + } + + Future 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-квоту. Рекомендации показываются из уже сохранённых рецептов. diff --git a/docs/plans/Summary.md b/docs/plans/Summary.md index 062e196..d9c92bc 100644 --- a/docs/plans/Summary.md +++ b/docs/plans/Summary.md @@ -5,10 +5,11 @@ | # | Итерация | Цель | Зависит от | |---|----------|------|------------| | 0 | Фундамент | Go-проект, БД, авторизация, Flutter-каркас | — | -| 1 | AI-рекомендации рецептов | Gemini генерирует рецепты, Pexels фото, сохранение рецептов | 0 | +| 1 | AI-рекомендации рецептов | GPT генерирует рецепты, Pexels фото, сохранение рецептов | 0 | | 2 | Управление продуктами | CRUD продуктов, сроки хранения, ingredient_mappings | 0 | -| 3 | Распознавание продуктов | OCR чека, фото продуктов, фото блюд (Gemini Vision) | 1, 2 | +| 3 | Распознавание продуктов | OCR чека, фото продуктов, фото блюд (GPT-4o Vision) | 1, 2 | | 4 | Планирование меню | Меню на неделю, AI-генерация, список покупок, дневник | 1, 2 | +| 5 | Главный экран | Дашборд: калории, план на сегодня, истекающие продукты, рекомендации | 1, 2, 4 | Дальнейшие итерации определяются приоритетами после MVP. Функциональность из TODO.md (дневник статистики, режим готовки, полировка) — следующий горизонт. @@ -36,11 +37,17 @@ ┌────────────────────┐ ┌─────────────────────┐ │ 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.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.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 @@ -222,22 +229,53 @@ | 4.9 | DiaryScreen | Записи за день, «+ Добавить» | ### Результат итерации -- Пользователь получает меню на неделю одним запросом к Gemini +- Пользователь получает меню на неделю одним запросом к GPT - Все рецепты меню сохраняются в 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 | |----------|------|-------------| | 0. Фундамент | Auth, профиль, каркас | Firebase | -| 1. AI-рекомендации | Рецепты + сохранение | Gemini, Pexels | +| 1. AI-рекомендации | Рецепты + сохранение | GPT-4o-mini, Pexels | | 2. Продукты | CRUD запасов, ingredient_mappings | — | -| 3. Распознавание | OCR чека, фото продуктов/блюда | Gemini Vision | -| 4. Меню | Недельное меню, список покупок | Gemini, Pexels | +| 3. Распознавание | OCR чека, фото продуктов/блюда | GPT-4o Vision | +| 4. Меню | Недельное меню, список покупок | GPT-4o-mini, Pexels | +| 5. Главный экран | Дашборд: калории, план, истекающие, рекомендации | — | **MVP:** итерации 0–2 (авторизация + рекомендации + продукты) — пользователь получает персонализированные рецепты.