From 79c32f226c76e81a58ead251cc2c4ff9123fda08 Mon Sep 17 00:00:00 2001 From: dbastrikin Date: Sun, 22 Feb 2026 15:59:09 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20add=20Iteration=206=20=E2=80=94=20profi?= =?UTF-8?q?le=20screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Describe ProfileService, profileProvider, ProfileScreen sections (avatar, body params, goal/activity, daily calories, logout), EditProfileSheet, and HomeScreen name integration. No new backend endpoints needed — GET/PUT /profile already exist. Co-Authored-By: Claude Sonnet 4.6 --- docs/plans/Iteration_6.md | 303 ++++++++++++++++++++++++++++++++++++++ docs/plans/Summary.md | 40 +++++ 2 files changed, 343 insertions(+) create mode 100644 docs/plans/Iteration_6.md diff --git a/docs/plans/Iteration_6.md b/docs/plans/Iteration_6.md new file mode 100644 index 0000000..2e06b9d --- /dev/null +++ b/docs/plans/Iteration_6.md @@ -0,0 +1,303 @@ +# Итерация 6: Экран профиля + +**Цель:** заменить заглушку «Раздел в разработке» полноценным экраном профиля — показать данные пользователя, дать возможность редактировать параметры тела и цели, отобразить рассчитанную норму калорий и выйти из аккаунта. + +**Зависимости:** итерация 0 (авторизация, модель User, `GET /profile`, `PUT /profile`). + +--- + +## Структура задач + +``` +6.1 Flutter: ProfileService + └── GET /profile, PUT /profile + +6.2 Flutter: profileProvider + └── StateNotifier, load(), update() + +6.3 Flutter: ProfileScreen + ├── 6.3.1 Секция аватара и имени + ├── 6.3.2 Секция параметров тела + ├── 6.3.3 Секция цели и активности + ├── 6.3.4 Секция нормы калорий + └── 6.3.5 Кнопка выхода + +6.4 Flutter: EditProfileSheet + └── Форма редактирования всех параметров + +6.5 Flutter: HomeScreen — имя из профиля +``` + +--- + +## 6.1 Backend: уже реализовано + +Новых эндпоинтов не требуется. Оба маршрута существуют: + +``` +GET /profile — возвращает полный профиль пользователя +PUT /profile — частичное обновление; если изменяются height/weight/age/gender/activity/goal, + daily_calories пересчитывается автоматически (Mifflin-St Jeor) +``` + +### GET /profile — ответ + +```json +{ + "id": "uuid", + "email": "user@example.com", + "name": "Алексей", + "avatar_url": null, + "height_cm": 175, + "weight_kg": 72.0, + "age": 28, + "gender": "male", + "activity": "moderate", + "goal": "maintain", + "daily_calories": 2480, + "plan": "free", + "preferences": {} +} +``` + +### PUT /profile — запрос (все поля опциональны) + +```json +{ + "name": "Алексей", + "height_cm": 175, + "weight_kg": 72.0, + "age": 28, + "gender": "male", + "activity": "moderate", + "goal": "maintain" +} +``` + +Допустимые значения: +- `gender`: `"male"` | `"female"` +- `activity`: `"low"` | `"moderate"` | `"high"` +- `goal`: `"lose"` | `"maintain"` | `"gain"` + +--- + +## 6.2 Flutter: ProfileService + +```dart +class ProfileService { + ProfileService(this._client); + final ApiClient _client; + + Future getProfile() async { + final json = await _client.get('/profile'); + return User.fromJson(json); + } + + Future updateProfile(UpdateProfileRequest req) async { + final json = await _client.put('/profile', data: req.toJson()); + return User.fromJson(json); + } +} + +class UpdateProfileRequest { + final String? name; + final int? heightCm; + final double? weightKg; + final int? age; + final String? gender; + final String? activity; + final String? goal; +} +``` + +--- + +## 6.3 Flutter: profileProvider + +```dart +final profileProvider = + StateNotifierProvider>( + (ref) => ProfileNotifier(ref.read(profileServiceProvider)), +); + +class ProfileNotifier extends StateNotifier> { + ProfileNotifier(this._service) : super(const AsyncValue.loading()) { + load(); + } + + Future load() async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() => _service.getProfile()); + } + + Future update(UpdateProfileRequest req) async { + try { + final updated = await _service.updateProfile(req); + state = AsyncValue.data(updated); + return true; + } catch (_) { + return false; + } + } +} +``` + +--- + +## 6.4 Flutter: ProfileScreen + +### Макет + +``` +┌─────────────────────────────────────┐ +│ Профиль Изменить │ ← AppBar +├─────────────────────────────────────┤ +│ │ +│ [○] Алексей │ ← аватар-заглушка + имя +│ user@example.com │ +│ │ +├─────────────────────────────────────┤ +│ ПАРАМЕТРЫ ТЕЛА │ +│ Рост 175 см › │ +│ Вес 72 кг › │ +│ Возраст 28 лет › │ +│ Пол Мужской › │ +├─────────────────────────────────────┤ +│ ЦЕЛЬ И АКТИВНОСТЬ │ +│ Цель Поддержание › │ +│ Активность Средняя › │ +├─────────────────────────────────────┤ +│ ПИТАНИЕ │ +│ Норма калорий 2 480 ккал │ +│ Рассчитано по параметрам тела │ +├─────────────────────────────────────┤ +│ │ +│ [ Выйти из аккаунта ] │ +│ │ +└─────────────────────────────────────┘ +``` + +### 6.3.1 Секция аватара и имени + +- Круглый контейнер с инициалами (первая буква имени) на фоне `AppColors.primary.withValues(alpha: 0.15)` — если `avatar_url` равен null +- Под контейнером: имя (`titleMedium`) и email (`bodySmall`, `textSecondary`) + +### 6.3.2 Секция параметров тела + +Сгруппированная карточка (`Card`) с `ListTile`-строками в стиле iOS: +- **Рост** — `${height} см` или «Не задано» серым +- **Вес** — `${weight} кг` или «Не задано» +- **Возраст** — `${age} лет` или «Не задано» +- **Пол** — «Мужской» / «Женский» / «Не задано» + +Строки — только для чтения. Редактирование открывается кнопкой **Изменить** в AppBar. + +Если все параметры не заданы — показать inline-баннер: +> «Укажите параметры тела для расчёта нормы калорий» + +### 6.3.3 Секция цели и активности + +Аналогичная карточка: +- **Цель**: «Похудение» | «Поддержание» | «Набор массы» +- **Активность**: «Низкая» | «Средняя» | «Высокая» + +### 6.3.4 Секция нормы калорий + +Карточка с одной строкой: +- Если `daily_calories != null` — показать «N ккал/день» +- Иначе — «Задайте параметры тела» + +Подпись: «Рассчитано автоматически по формуле Миффлина-Сан Жеора». + +### 6.3.5 Кнопка выхода + +`OutlinedButton` в цвете `AppColors.error`, по всей ширине. +Перед выходом — `AlertDialog` с подтверждением. +По подтверждению: `ref.read(authProvider.notifier).signOut()`. + +--- + +## 6.5 Flutter: EditProfileSheet + +Открывается из кнопки «Изменить» в AppBar — `showModalBottomSheet` с прокрутой. + +### Поля формы + +| Поле | Тип ввода | Валидация | +|------|-----------|-----------| +| Имя | TextFormField | не пустое | +| Рост | TextFormField (число) | 100–250 | +| Вес | TextFormField (десятичное) | 30–300 | +| Возраст | TextFormField (число) | 10–120 | +| Пол | SegmentedButton: «Мужской» / «Женский» | — | +| Цель | SegmentedButton: «Похудение» / «Поддержание» / «Набор» | — | +| Активность | SegmentedButton: «Низкая» / «Средняя» / «Высокая» | — | + +### Поведение + +- Поля предзаполнены текущими значениями из `profileProvider` +- Кнопка «Сохранить» (FilledButton) — вызывает `profileProvider.notifier.update(...)` +- При успехе — закрывает sheet, показывает `SnackBar` «Профиль обновлён» +- При ошибке — показывает `SnackBar` «Не удалось сохранить» +- Калории пересчитываются на сервере автоматически — UI обновится из ответа + +### Макет sheet + +``` +┌─────────────────────────────────────┐ +│ ── (drag handle) │ +│ Редактировать профиль │ +├─────────────────────────────────────┤ +│ Имя │ +│ [Алексей ] │ +│ │ +│ Рост (см) Вес (кг) │ +│ [175 ] [72.0 ] │ +│ │ +│ Возраст (лет) │ +│ [28 ] │ +│ │ +│ Пол │ +│ [ Мужской ● | Женский ] │ +│ │ +│ Цель │ +│ [ Похудение | Поддержание● | Набор ]│ +│ │ +│ Активность │ +│ [ Низкая | Средняя● | Высокая ] │ +│ │ +│ [ Сохранить ] │ +└─────────────────────────────────────┘ +``` + +--- + +## 6.6 Flutter: HomeScreen — имя пользователя + +Приветствие на главном экране («Доброе утро, Алексей!») сейчас использует статичное имя или имя из `authProvider`. + +После этой итерации: имя берётся из `profileProvider`: +```dart +final user = ref.watch(profileProvider).valueOrNull; +final name = user?.name ?? ''; +``` + +--- + +## Оценка нагрузки + +| Действие | OpenAI | БД запросов | +|---|---|---| +| Открытие вкладки Профиль | 0 | 1 (SELECT users) | +| Сохранение профиля | 0 | 1–2 (UPDATE + SELECT) | + +Экран профиля не тратит AI-квоту. + +--- + +## Результат итерации + +- Пользователь видит своё имя, параметры тела и цели +- Может обновить параметры — норма калорий пересчитывается мгновенно +- Может выйти из аккаунта +- Приветствие на главном экране показывает реальное имя diff --git a/docs/plans/Summary.md b/docs/plans/Summary.md index d9c92bc..a439fb4 100644 --- a/docs/plans/Summary.md +++ b/docs/plans/Summary.md @@ -10,6 +10,7 @@ | 3 | Распознавание продуктов | OCR чека, фото продуктов, фото блюд (GPT-4o Vision) | 1, 2 | | 4 | Планирование меню | Меню на неделю, AI-генерация, список покупок, дневник | 1, 2 | | 5 | Главный экран | Дашборд: калории, план на сегодня, истекающие продукты, рекомендации | 1, 2, 4 | +| 6 | Профиль | Просмотр и редактирование профиля, норма калорий, выход из аккаунта | 0 | Дальнейшие итерации определяются приоритетами после MVP. Функциональность из TODO.md (дневник статистики, режим готовки, полировка) — следующий горизонт. @@ -45,6 +46,12 @@ │ 5. Главный экран │ │ (дашборд) │ └─────────────────────┘ + +┌─────────────────────┐ +│ 6. Профиль │ (зависит только от 0) +│ (просмотр + │ +│ редактирование) │ +└─────────────────────┘ ``` **Параллельная разработка:** итерации 1 и 2 могут выполняться параллельно. Итерации 3 и 4 — тоже параллельно после завершения 1 и 2. Итерация 5 — после 4. @@ -266,6 +273,38 @@ --- +## Итерация 6: Профиль + +> **Детальный план:** [Iteration_6.md](./Iteration_6.md) + +**Цель:** заменить заглушку вкладки «Профиль» полноценным экраном — просмотр данных, редактирование параметров тела и целей, отображение расчётной нормы калорий, выход из аккаунта. + +**Зависимости:** итерация 0. + +### User Stories + +#### Backend + +Новых эндпоинтов не требуется — `GET /profile` и `PUT /profile` реализованы в итерации 0. + +#### Flutter + +| ID | Story | Описание | +|----|-------|----------| +| 6.1 | ProfileService | `getProfile()` → `GET /profile`, `updateProfile()` → `PUT /profile` | +| 6.2 | profileProvider | `StateNotifier>`, методы `load()` и `update()` | +| 6.3 | ProfileScreen | Секции: аватар+имя, параметры тела, цель+активность, норма калорий, кнопка выхода | +| 6.4 | EditProfileSheet | Modal bottom sheet с формой редактирования всех параметров | +| 6.5 | HomeScreen — имя | Приветствие берёт имя из `profileProvider` вместо захардкоженного | + +### Результат итерации +- Пользователь видит своё имя, email, параметры тела и цели +- Может обновить параметры — норма калорий пересчитывается автоматически сервером +- Может выйти из аккаунта с подтверждением +- Приветствие на главном экране показывает реальное имя пользователя + +--- + ## Итоги | Итерация | Цель | Ключевые API | @@ -276,6 +315,7 @@ | 3. Распознавание | OCR чека, фото продуктов/блюда | GPT-4o Vision | | 4. Меню | Недельное меню, список покупок | GPT-4o-mini, Pexels | | 5. Главный экран | Дашборд: калории, план, истекающие, рекомендации | — | +| 6. Профиль | Параметры тела, цель, норма калорий, выход | — | **MVP:** итерации 0–2 (авторизация + рекомендации + продукты) — пользователь получает персонализированные рецепты.