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

11 KiB
Raw Permalink Blame History

Итерация 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>

Ответ

{
  "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

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