docs: add Iteration 6 — profile screen
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 <noreply@anthropic.com>
This commit is contained in:
303
docs/plans/Iteration_6.md
Normal file
303
docs/plans/Iteration_6.md
Normal file
@@ -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<User> getProfile() async {
|
||||||
|
final json = await _client.get('/profile');
|
||||||
|
return User.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<User> 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<ProfileNotifier, AsyncValue<User>>(
|
||||||
|
(ref) => ProfileNotifier(ref.read(profileServiceProvider)),
|
||||||
|
);
|
||||||
|
|
||||||
|
class ProfileNotifier extends StateNotifier<AsyncValue<User>> {
|
||||||
|
ProfileNotifier(this._service) : super(const AsyncValue.loading()) {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> load() async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
state = await AsyncValue.guard(() => _service.getProfile());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> 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-квоту.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Результат итерации
|
||||||
|
|
||||||
|
- Пользователь видит своё имя, параметры тела и цели
|
||||||
|
- Может обновить параметры — норма калорий пересчитывается мгновенно
|
||||||
|
- Может выйти из аккаунта
|
||||||
|
- Приветствие на главном экране показывает реальное имя
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
| 3 | Распознавание продуктов | OCR чека, фото продуктов, фото блюд (GPT-4o Vision) | 1, 2 |
|
| 3 | Распознавание продуктов | OCR чека, фото продуктов, фото блюд (GPT-4o Vision) | 1, 2 |
|
||||||
| 4 | Планирование меню | Меню на неделю, AI-генерация, список покупок, дневник | 1, 2 |
|
| 4 | Планирование меню | Меню на неделю, AI-генерация, список покупок, дневник | 1, 2 |
|
||||||
| 5 | Главный экран | Дашборд: калории, план на сегодня, истекающие продукты, рекомендации | 1, 2, 4 |
|
| 5 | Главный экран | Дашборд: калории, план на сегодня, истекающие продукты, рекомендации | 1, 2, 4 |
|
||||||
|
| 6 | Профиль | Просмотр и редактирование профиля, норма калорий, выход из аккаунта | 0 |
|
||||||
|
|
||||||
Дальнейшие итерации определяются приоритетами после MVP. Функциональность из TODO.md (дневник статистики, режим готовки, полировка) — следующий горизонт.
|
Дальнейшие итерации определяются приоритетами после MVP. Функциональность из TODO.md (дневник статистики, режим готовки, полировка) — следующий горизонт.
|
||||||
|
|
||||||
@@ -45,6 +46,12 @@
|
|||||||
│ 5. Главный экран │
|
│ 5. Главный экран │
|
||||||
│ (дашборд) │
|
│ (дашборд) │
|
||||||
└─────────────────────┘
|
└─────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ 6. Профиль │ (зависит только от 0)
|
||||||
|
│ (просмотр + │
|
||||||
|
│ редактирование) │
|
||||||
|
└─────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
**Параллельная разработка:** итерации 1 и 2 могут выполняться параллельно. Итерации 3 и 4 — тоже параллельно после завершения 1 и 2. Итерация 5 — после 4.
|
**Параллельная разработка:** итерации 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<AsyncValue<User>>`, методы `load()` и `update()` |
|
||||||
|
| 6.3 | ProfileScreen | Секции: аватар+имя, параметры тела, цель+активность, норма калорий, кнопка выхода |
|
||||||
|
| 6.4 | EditProfileSheet | Modal bottom sheet с формой редактирования всех параметров |
|
||||||
|
| 6.5 | HomeScreen — имя | Приветствие берёт имя из `profileProvider` вместо захардкоженного |
|
||||||
|
|
||||||
|
### Результат итерации
|
||||||
|
- Пользователь видит своё имя, email, параметры тела и цели
|
||||||
|
- Может обновить параметры — норма калорий пересчитывается автоматически сервером
|
||||||
|
- Может выйти из аккаунта с подтверждением
|
||||||
|
- Приветствие на главном экране показывает реальное имя пользователя
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Итоги
|
## Итоги
|
||||||
|
|
||||||
| Итерация | Цель | Ключевые API |
|
| Итерация | Цель | Ключевые API |
|
||||||
@@ -276,6 +315,7 @@
|
|||||||
| 3. Распознавание | OCR чека, фото продуктов/блюда | GPT-4o Vision |
|
| 3. Распознавание | OCR чека, фото продуктов/блюда | GPT-4o Vision |
|
||||||
| 4. Меню | Недельное меню, список покупок | GPT-4o-mini, Pexels |
|
| 4. Меню | Недельное меню, список покупок | GPT-4o-mini, Pexels |
|
||||||
| 5. Главный экран | Дашборд: калории, план, истекающие, рекомендации | — |
|
| 5. Главный экран | Дашборд: калории, план, истекающие, рекомендации | — |
|
||||||
|
| 6. Профиль | Параметры тела, цель, норма калорий, выход | — |
|
||||||
|
|
||||||
**MVP:** итерации 0–2 (авторизация + рекомендации + продукты) — пользователь получает персонализированные рецепты.
|
**MVP:** итерации 0–2 (авторизация + рекомендации + продукты) — пользователь получает персонализированные рецепты.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user