# Итерация 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-квоту. --- ## Результат итерации - Пользователь видит своё имя, параметры тела и цели - Может обновить параметры — норма калорий пересчитывается мгновенно - Может выйти из аккаунта - Приветствие на главном экране показывает реальное имя