# Итерация 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-квоту. Рекомендации показываются из уже сохранённых рецептов.