import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/auth/auth_provider.dart'; import '../../core/locale/language_provider.dart'; import '../../core/theme/app_colors.dart'; import '../../shared/models/meal_type.dart'; import '../../shared/models/user.dart'; import 'profile_provider.dart'; import 'profile_service.dart'; class ProfileScreen extends ConsumerWidget { const ProfileScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(profileProvider); return Scaffold( appBar: AppBar( title: const Text('Профиль'), actions: [ if (state.hasValue) TextButton( onPressed: () => _openEdit(context, state.value!), child: const Text('Изменить'), ), ], ), body: state.when( loading: () => const Center(child: CircularProgressIndicator()), error: (_, __) => Center( child: FilledButton( onPressed: () => ref.read(profileProvider.notifier).load(), child: const Text('Повторить'), ), ), data: (user) => _ProfileBody(user: user), ), ); } void _openEdit(BuildContext context, User user) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => EditProfileSheet(user: user), ); } } // ── Profile body ────────────────────────────────────────────── class _ProfileBody extends ConsumerWidget { final User user; const _ProfileBody({required this.user}); @override Widget build(BuildContext context, WidgetRef ref) { return ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), children: [ _ProfileHeader(user: user), const SizedBox(height: 24), // Body params _SectionLabel('ПАРАМЕТРЫ ТЕЛА'), const SizedBox(height: 6), _InfoCard(children: [ _InfoRow('Рост', user.heightCm != null ? '${user.heightCm} см' : null), const Divider(height: 1, indent: 16), _InfoRow('Вес', user.weightKg != null ? '${_fmt(user.weightKg!)} кг' : null), const Divider(height: 1, indent: 16), _InfoRow('Возраст', user.age != null ? '${user.age} лет' : null), const Divider(height: 1, indent: 16), _InfoRow('Пол', _genderLabel(user.gender)), ]), if (user.heightCm == null && user.weightKg == null) ...[ const SizedBox(height: 8), _HintBanner('Укажите параметры тела для расчёта нормы калорий'), ], const SizedBox(height: 16), // Goal & activity _SectionLabel('ЦЕЛЬ И АКТИВНОСТЬ'), const SizedBox(height: 6), _InfoCard(children: [ _InfoRow('Цель', _goalLabel(user.goal)), const Divider(height: 1, indent: 16), _InfoRow('Активность', _activityLabel(user.activity)), ]), const SizedBox(height: 16), // Calories + meal types _SectionLabel('ПИТАНИЕ'), const SizedBox(height: 6), _InfoCard(children: [ _InfoRow( 'Норма калорий', user.dailyCalories != null ? '${user.dailyCalories} ккал/день' : null, ), const Divider(height: 1, indent: 16), _InfoRow( 'Приёмы пищи', user.mealTypes .map((mealTypeId) => mealTypeById(mealTypeId)?.label ?? mealTypeId) .join(', '), ), ]), if (user.dailyCalories != null) ...[ const SizedBox(height: 4), Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Text( 'Рассчитано по формуле Миффлина-Сан Жеора', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: AppColors.textSecondary, ), ), ), ], const SizedBox(height: 16), // Settings _SectionLabel('НАСТРОЙКИ'), const SizedBox(height: 6), _InfoCard(children: [ _InfoRow( 'Язык', ref.watch(supportedLanguagesProvider).valueOrNull?[ user.preferences['language'] as String? ?? 'ru'] ?? 'Русский', ), ]), const SizedBox(height: 32), _LogoutButton(), ], ); } static String _fmt(double w) => w == w.truncateToDouble() ? w.toInt().toString() : w.toStringAsFixed(1); static String? _genderLabel(String? g) => switch (g) { 'male' => 'Мужской', 'female' => 'Женский', _ => null, }; static String? _goalLabel(String? g) => switch (g) { 'lose' => 'Похудение', 'maintain' => 'Поддержание', 'gain' => 'Набор массы', _ => null, }; static String? _activityLabel(String? a) => switch (a) { 'low' => 'Низкая', 'moderate' => 'Средняя', 'high' => 'Высокая', _ => null, }; } // ── Header ──────────────────────────────────────────────────── class _ProfileHeader extends StatelessWidget { final User user; const _ProfileHeader({required this.user}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final initials = user.name.isNotEmpty ? user.name[0].toUpperCase() : '?'; return Column( children: [ const SizedBox(height: 8), CircleAvatar( radius: 40, backgroundColor: AppColors.primary.withValues(alpha: 0.15), child: Text( initials, style: theme.textTheme.headlineMedium?.copyWith( color: AppColors.primary, fontWeight: FontWeight.w600, ), ), ), const SizedBox(height: 12), Text(user.name, style: theme.textTheme.titleMedium), const SizedBox(height: 2), Text( user.email, style: theme.textTheme.bodySmall ?.copyWith(color: AppColors.textSecondary), ), ], ); } } // ── Shared widgets ──────────────────────────────────────────── class _SectionLabel extends StatelessWidget { final String text; const _SectionLabel(this.text); @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.only(left: 4), child: Text( text, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: AppColors.textSecondary, letterSpacing: 0.5, ), ), ); } class _InfoCard extends StatelessWidget { final List children; const _InfoCard({required this.children}); @override Widget build(BuildContext context) => Card(child: Column(children: children)); } class _InfoRow extends StatelessWidget { final String label; final String? value; const _InfoRow(this.label, this.value); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), child: Row( children: [ Text(label, style: theme.textTheme.bodyMedium), const Spacer(), Text( value ?? 'Не задано', style: theme.textTheme.bodyMedium?.copyWith( color: value != null ? AppColors.textPrimary : AppColors.textSecondary, ), ), ], ), ); } } class _HintBanner extends StatelessWidget { final String text; const _HintBanner(this.text); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: AppColors.warning.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ const Icon(Icons.info_outline, size: 16, color: AppColors.warning), const SizedBox(width: 8), Expanded( child: Text( text, style: Theme.of(context) .textTheme .bodySmall ?.copyWith(color: AppColors.warning), ), ), ], ), ); } } class _LogoutButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return OutlinedButton( style: OutlinedButton.styleFrom( foregroundColor: AppColors.error, side: const BorderSide(color: AppColors.error), ), onPressed: () => _confirmLogout(context, ref), child: const Text('Выйти из аккаунта'), ); } Future _confirmLogout(BuildContext context, WidgetRef ref) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Выйти из аккаунта?'), content: const Text('Вы будете перенаправлены на экран входа.'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Отмена'), ), TextButton( style: TextButton.styleFrom(foregroundColor: AppColors.error), onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Выйти'), ), ], ), ); if (confirmed == true) { await ref.read(authProvider.notifier).signOut(); } } } // ── Edit sheet ──────────────────────────────────────────────── class EditProfileSheet extends ConsumerStatefulWidget { final User user; const EditProfileSheet({super.key, required this.user}); @override ConsumerState createState() => _EditProfileSheetState(); } class _EditProfileSheetState extends ConsumerState { final _formKey = GlobalKey(); late final TextEditingController _nameCtrl; late final TextEditingController _heightCtrl; late final TextEditingController _weightCtrl; DateTime? _selectedDob; String? _gender; String? _goal; String? _activity; String? _language; List _mealTypes = []; bool _saving = false; @override void initState() { super.initState(); final u = widget.user; _nameCtrl = TextEditingController(text: u.name); _heightCtrl = TextEditingController(text: u.heightCm?.toString() ?? ''); _weightCtrl = TextEditingController( text: u.weightKg != null ? _fmt(u.weightKg!) : ''); _selectedDob = u.dateOfBirth != null ? DateTime.tryParse(u.dateOfBirth!) : null; _gender = u.gender; _goal = u.goal; _activity = u.activity; _language = u.preferences['language'] as String? ?? 'ru'; _mealTypes = List.from(u.mealTypes); } @override void dispose() { _nameCtrl.dispose(); _heightCtrl.dispose(); _weightCtrl.dispose(); super.dispose(); } String _fmt(double w) => w == w.truncateToDouble() ? w.toInt().toString() : w.toStringAsFixed(1); Future _pickDob() async { final now = DateTime.now(); final initial = _selectedDob ?? DateTime(now.year - 25, now.month, now.day); final picked = await showDatePicker( context: context, initialDate: initial, firstDate: DateTime(now.year - 120), lastDate: DateTime(now.year - 10, now.month, now.day), ); if (picked != null) setState(() => _selectedDob = picked); } Future _save() async { if (!_formKey.currentState!.validate()) return; setState(() => _saving = true); final req = UpdateProfileRequest( name: _nameCtrl.text.trim(), heightCm: int.tryParse(_heightCtrl.text), weightKg: double.tryParse(_weightCtrl.text), dateOfBirth: _selectedDob != null ? '${_selectedDob!.year.toString().padLeft(4, '0')}-' '${_selectedDob!.month.toString().padLeft(2, '0')}-' '${_selectedDob!.day.toString().padLeft(2, '0')}' : null, gender: _gender, goal: _goal, activity: _activity, language: _language, mealTypes: _mealTypes, ); final ok = await ref.read(profileProvider.notifier).update(req); if (!mounted) return; setState(() => _saving = false); if (ok) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Профиль обновлён')), ); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Не удалось сохранить')), ); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final bottomInset = MediaQuery.viewInsetsOf(context).bottom; return Padding( padding: EdgeInsets.only(bottom: bottomInset), child: DraggableScrollableSheet( expand: false, initialChildSize: 0.85, maxChildSize: 0.95, builder: (_, controller) => Column( children: [ const SizedBox(height: 12), // Drag handle Container( width: 36, height: 4, decoration: BoxDecoration( color: AppColors.separator, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ Text('Редактировать профиль', style: theme.textTheme.titleMedium), const Spacer(), TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Отмена'), ), ], ), ), const Divider(height: 1), Expanded( child: Form( key: _formKey, child: ListView( controller: controller, padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), children: [ // Name TextFormField( controller: _nameCtrl, decoration: const InputDecoration(labelText: 'Имя'), textCapitalization: TextCapitalization.words, validator: (v) => (v == null || v.trim().isEmpty) ? 'Введите имя' : null, ), const SizedBox(height: 12), // Height + Weight Row( children: [ Expanded( child: TextFormField( controller: _heightCtrl, decoration: const InputDecoration(labelText: 'Рост (см)'), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly ], validator: (v) { if (v == null || v.isEmpty) return null; final n = int.tryParse(v); if (n == null || n < 100 || n > 250) { return '100–250'; } return null; }, ), ), const SizedBox(width: 12), Expanded( child: TextFormField( controller: _weightCtrl, decoration: const InputDecoration(labelText: 'Вес (кг)'), keyboardType: const TextInputType.numberWithOptions( decimal: true), inputFormatters: [ FilteringTextInputFormatter.allow( RegExp(r'[0-9.]')) ], validator: (v) { if (v == null || v.isEmpty) return null; final n = double.tryParse(v); if (n == null || n < 30 || n > 300) { return '30–300'; } return null; }, ), ), ], ), const SizedBox(height: 12), // Date of birth InkWell( onTap: _pickDob, child: InputDecorator( decoration: const InputDecoration(labelText: 'Дата рождения'), child: Text( _selectedDob != null ? '${_selectedDob!.day.toString().padLeft(2, '0')}.' '${_selectedDob!.month.toString().padLeft(2, '0')}.' '${_selectedDob!.year}' : 'Не задано', style: Theme.of(context).textTheme.bodyMedium, ), ), ), const SizedBox(height: 20), // Gender Text('Пол', style: theme.textTheme.labelMedium), const SizedBox(height: 8), SegmentedButton( segments: const [ ButtonSegment(value: 'male', label: Text('Мужской')), ButtonSegment(value: 'female', label: Text('Женский')), ], selected: _gender != null ? {_gender!} : const {}, emptySelectionAllowed: true, onSelectionChanged: (s) => setState(() => _gender = s.isEmpty ? null : s.first), ), const SizedBox(height: 20), // Goal Text('Цель', style: theme.textTheme.labelMedium), const SizedBox(height: 8), SegmentedButton( segments: const [ ButtonSegment(value: 'lose', label: Text('Похудение')), ButtonSegment( value: 'maintain', label: Text('Поддержание')), ButtonSegment(value: 'gain', label: Text('Набор')), ], selected: _goal != null ? {_goal!} : const {}, emptySelectionAllowed: true, onSelectionChanged: (s) => setState(() => _goal = s.isEmpty ? null : s.first), ), const SizedBox(height: 20), // Activity Text('Активность', style: theme.textTheme.labelMedium), const SizedBox(height: 8), SegmentedButton( segments: const [ ButtonSegment(value: 'low', label: Text('Низкая')), ButtonSegment( value: 'moderate', label: Text('Средняя')), ButtonSegment(value: 'high', label: Text('Высокая')), ], selected: _activity != null ? {_activity!} : const {}, emptySelectionAllowed: true, onSelectionChanged: (s) => setState( () => _activity = s.isEmpty ? null : s.first), ), const SizedBox(height: 20), // Meal types Text('Приёмы пищи', style: theme.textTheme.labelMedium), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 6, children: kAllMealTypes.map((mealTypeOption) { final isSelected = _mealTypes.contains(mealTypeOption.id); return FilterChip( label: Text( '${mealTypeOption.emoji} ${mealTypeOption.label}'), selected: isSelected, onSelected: (selected) { setState(() { if (selected) { _mealTypes.add(mealTypeOption.id); } else if (_mealTypes.length > 1) { _mealTypes.remove(mealTypeOption.id); } }); }, ); }).toList(), ), const SizedBox(height: 20), // Language ref.watch(supportedLanguagesProvider).when( data: (languages) => DropdownButtonFormField( decoration: const InputDecoration( labelText: 'Язык интерфейса'), initialValue: _language, items: languages.entries .map((e) => DropdownMenuItem( value: e.key, child: Text(e.value), )) .toList(), onChanged: (v) => setState(() => _language = v), ), loading: () => const Center( child: CircularProgressIndicator()), error: (_, __) => const Text('Не удалось загрузить языки'), ), const SizedBox(height: 32), // Save FilledButton( onPressed: _saving ? null : _save, child: _saving ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white), ) : const Text('Сохранить'), ), ], ), ), ), ], ), ), ); } }