import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:userfront/i18n.dart'; import '../../../../core/notifiers/auth_notifier.dart'; import '../../../../core/i18n/locale_utils.dart'; import '../../../../core/services/auth_token_store.dart'; import '../../../../core/ui/layout_breakpoints.dart'; import '../../../../core/widgets/language_selector.dart'; import '../../data/models/user_profile_model.dart'; import '../../domain/notifiers/profile_notifier.dart'; class ProfilePage extends ConsumerStatefulWidget { const ProfilePage({super.key}); @override ConsumerState createState() => _ProfilePageState(); } class _ProfilePageState extends ConsumerState { static const _ink = Color(0xFF1A1F2C); static const _surface = Colors.white; static const _border = Color(0xFFE5E7EB); static const _subtle = Color(0xFFF7F8FA); UserProfile? _cachedProfile; String? _editingField; TextEditingController? _nameController; TextEditingController? _phoneController; TextEditingController? _departmentController; TextEditingController? _codeController; TextEditingController? _currentPasswordController; TextEditingController? _newPasswordController; TextEditingController? _confirmPasswordController; final FocusNode _nameFocus = FocusNode(); final FocusNode _departmentFocus = FocusNode(); final FocusNode _phoneFocus = FocusNode(); final FocusNode _phoneCodeFocus = FocusNode(); bool _nameTouched = false; bool _departmentTouched = false; bool _phoneTouched = false; bool _phoneCodeTouched = false; bool _isSavingField = false; String _initialPhone = ''; bool _isPhoneChanged = false; bool _isPhoneVerified = false; bool _isCodeSent = false; bool _isVerifying = false; bool _isPasswordSaving = false; String? _passwordError; String? _passwordSuccess; bool _showCurrentPassword = false; bool _showNewPassword = false; bool _showConfirmPassword = false; @override void initState() { super.initState(); _nameFocus.addListener(_onNameFocusChange); _departmentFocus.addListener(_onDepartmentFocusChange); _phoneFocus.addListener(_onPhoneFocusChange); _phoneCodeFocus.addListener(_onPhoneCodeFocusChange); } void _onNameFocusChange() { if (!mounted) return; if (!_nameFocus.hasFocus && _nameTouched) { final profile = ref.read(profileProvider).value ?? _cachedProfile; if (profile != null) _autoSaveIfEditing(profile, 'name'); } else if (_nameFocus.hasFocus) { _nameTouched = true; } } void _onDepartmentFocusChange() { if (!mounted) return; if (!_departmentFocus.hasFocus && _departmentTouched) { final profile = ref.read(profileProvider).value ?? _cachedProfile; if (profile != null) _autoSaveIfEditing(profile, 'department'); } else if (_departmentFocus.hasFocus) { _departmentTouched = true; } } void _onPhoneFocusChange() { if (!mounted) return; if (!_phoneFocus.hasFocus && _phoneTouched) { final profile = ref.read(profileProvider).value ?? _cachedProfile; if (profile != null) _handlePhoneFocusChange(profile); } else if (_phoneFocus.hasFocus) { _phoneTouched = true; } } void _onPhoneCodeFocusChange() { if (!mounted) return; if (!_phoneCodeFocus.hasFocus && _phoneCodeTouched) { final profile = ref.read(profileProvider).value ?? _cachedProfile; if (profile != null) _handlePhoneFocusChange(profile); } else if (_phoneCodeFocus.hasFocus) { _phoneCodeTouched = true; } } @override void dispose() { _nameController?.dispose(); _phoneController?.dispose(); _departmentController?.dispose(); _codeController?.dispose(); _currentPasswordController?.dispose(); _newPasswordController?.dispose(); _confirmPasswordController?.dispose(); _nameFocus.dispose(); _departmentFocus.dispose(); _phoneFocus.dispose(); _phoneCodeFocus.dispose(); super.dispose(); } Future _logout() async { AuthTokenStore.clear(); AuthNotifier.instance.notify(); } void _ensureControllers(UserProfile profile) { _nameController ??= TextEditingController(text: profile.name); _departmentController ??= TextEditingController(text: profile.department); _codeController ??= TextEditingController(); _currentPasswordController ??= TextEditingController(); _newPasswordController ??= TextEditingController(); _confirmPasswordController ??= TextEditingController(); if (_phoneController == null) { _phoneController = TextEditingController(text: profile.phone); _initialPhone = profile.phone; _phoneController!.addListener(_onPhoneChanged); } if (_editingField != 'name' && _nameController!.text != profile.name) { _nameController!.text = profile.name; } if (_editingField != 'department' && _departmentController!.text != profile.department) { _departmentController!.text = profile.department; } if (_editingField != 'phone' && _phoneController!.text != profile.phone) { _phoneController!.text = profile.phone; _initialPhone = profile.phone; _resetPhoneState(); } } void _onPhoneChanged() { if (_phoneController == null) return; final changed = _phoneController!.text != _initialPhone; if (changed != _isPhoneChanged) { setState(() { _isPhoneChanged = changed; if (_isPhoneChanged) { _isPhoneVerified = false; _isCodeSent = false; _codeController?.clear(); } }); } } void _resetPhoneState() { _isPhoneChanged = false; _isPhoneVerified = false; _isCodeSent = false; _isVerifying = false; _codeController?.clear(); _phoneTouched = false; _phoneCodeTouched = false; } void _startEditing(String field, UserProfile profile) { setState(() { _editingField = field; if (field == 'name') { _nameController?.text = profile.name; } else if (field == 'department') { _departmentController?.text = profile.department; } else if (field == 'phone') { _phoneController?.text = profile.phone; _initialPhone = profile.phone; _resetPhoneState(); } }); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; if (field == 'name') { FocusScope.of(context).requestFocus(_nameFocus); } else if (field == 'department') { FocusScope.of(context).requestFocus(_departmentFocus); } else if (field == 'phone') { FocusScope.of(context).requestFocus(_phoneFocus); } }); } void _cancelEditing(UserProfile profile) { setState(() { if (_editingField == 'name') { _nameController?.text = profile.name; } else if (_editingField == 'department') { _departmentController?.text = profile.department; } else if (_editingField == 'phone') { _phoneController?.text = profile.phone; _initialPhone = profile.phone; _resetPhoneState(); } _editingField = null; _nameTouched = false; _departmentTouched = false; }); } Future _sendCode() async { final phone = _phoneController?.text ?? ''; if (phone.isEmpty) return; setState(() => _isVerifying = true); try { await ref.read(profileRepositoryProvider).sendUpdateCode(phone); setState(() { _isCodeSent = true; _isVerifying = false; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('msg.userfront.profile.phone.code_sent'))), ); } } catch (e) { setState(() => _isVerifying = false); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( tr( 'msg.userfront.profile.phone.send_failed', params: {'error': e.toString()}, ), ), ), ); } } } Future _verifyCode(UserProfile profile) async { final phone = _phoneController?.text ?? ''; final code = _codeController?.text ?? ''; if (code.isEmpty) return; setState(() => _isVerifying = true); try { await ref.read(profileRepositoryProvider).verifyUpdateCode(phone, code); setState(() { _isPhoneVerified = true; _isVerifying = false; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('msg.userfront.profile.phone.verified'))), ); } if (_editingField == 'phone') { await _saveField(profile); } } catch (e) { setState(() => _isVerifying = false); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( tr( 'msg.userfront.profile.phone.verify_failed', params: {'error': e.toString()}, ), ), ), ); } } } Future _changePassword() async { if (_isPasswordSaving) return; final currentPassword = _currentPasswordController?.text.trim() ?? ''; final newPassword = _newPasswordController?.text.trim() ?? ''; final confirmPassword = _confirmPasswordController?.text.trim() ?? ''; if (currentPassword.isEmpty) { setState( () => _passwordError = tr( 'msg.userfront.profile.password.current_required', ), ); return; } if (newPassword.isEmpty) { setState( () => _passwordError = tr('msg.userfront.profile.password.new_required'), ); return; } if (newPassword != confirmPassword) { setState( () => _passwordError = tr('msg.userfront.profile.password.mismatch'), ); return; } setState(() { _passwordError = null; _passwordSuccess = null; _isPasswordSaving = true; }); try { await ref .read(profileRepositoryProvider) .changePassword( currentPassword: currentPassword, newPassword: newPassword, ); _currentPasswordController?.clear(); _newPasswordController?.clear(); _confirmPasswordController?.clear(); setState(() { _passwordSuccess = tr('msg.userfront.profile.password.changed'); }); } catch (e) { final message = e.toString().replaceFirst('Exception: ', ''); setState(() { _passwordError = tr( 'msg.userfront.profile.password.change_failed', params: {'error': message}, ); }); } finally { if (mounted) { setState(() => _isPasswordSaving = false); } } } void _autoSaveIfEditing(UserProfile profile, String field) { if (_editingField != field) return; if (_isVerifying) return; if (_isSavingField) return; if (!_hasFieldChanged(profile, field)) { setState(() { if (field == 'phone') { _resetPhoneState(); } _editingField = null; if (field == 'name') { _nameTouched = false; } else if (field == 'department') { _departmentTouched = false; } }); return; } _saveField(profile); } void _handlePhoneFocusChange(UserProfile profile) { if (_editingField != 'phone') return; if (_isVerifying) return; if (_isSavingField) return; if (_phoneFocus.hasFocus || _phoneCodeFocus.hasFocus) return; if (!_hasFieldChanged(profile, 'phone')) { setState(() { _resetPhoneState(); _editingField = null; }); return; } _saveField(profile); } bool _hasFieldChanged(UserProfile profile, String field) { if (field == 'name') { return (_nameController?.text.trim() ?? '') != profile.name; } if (field == 'department') { return (_departmentController?.text.trim() ?? '') != profile.department; } if (field == 'phone') { return (_phoneController?.text.trim() ?? '') != profile.phone; } return false; } Future _saveField(UserProfile profile) async { if (_editingField == null) return; if (_isSavingField) return; final nextName = _editingField == 'name' ? _nameController!.text.trim() : profile.name; final nextPhone = _editingField == 'phone' ? _phoneController!.text.trim() : profile.phone; final nextDepartment = _editingField == 'department' ? _departmentController!.text.trim() : profile.department; if (_editingField == 'name' && nextName.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('msg.userfront.profile.name_required'))), ); return; } if (_editingField == 'department' && nextDepartment.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(tr('msg.userfront.profile.department_required')), ), ); return; } if (_editingField == 'phone') { if (nextPhone.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('msg.userfront.profile.phone_required'))), ); return; } if (_isPhoneChanged && !_isPhoneVerified) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(tr('msg.userfront.profile.phone_verify_required')), ), ); return; } } if (!_hasFieldChanged(profile, _editingField!)) { setState(() { if (_editingField == 'phone') { _resetPhoneState(); } _editingField = null; _nameTouched = false; _departmentTouched = false; }); return; } _isSavingField = true; try { await ref .read(profileProvider.notifier) .updateProfile( name: nextName, phone: nextPhone, department: nextDepartment, ); if (mounted) { setState(() { if (_editingField == 'phone') { _initialPhone = nextPhone; _resetPhoneState(); } _editingField = null; _nameTouched = false; _departmentTouched = false; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(tr('msg.userfront.profile.update_success'))), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( tr( 'msg.userfront.profile.update_failed', params: {'error': e.toString()}, ), ), ), ); } } finally { _isSavingField = false; } } Widget _buildSideMenu(BuildContext context) { return Column( children: [ Expanded( child: ListView( padding: const EdgeInsets.symmetric(vertical: 12), children: [ ListTile( leading: const Icon(Icons.home_outlined), title: Text(tr('ui.userfront.nav.dashboard')), onTap: () => context.go(buildLocalizedHomePath(Uri.base)), ), ListTile( leading: const Icon(Icons.person_outline), title: Text(tr('ui.userfront.nav.profile')), selected: true, onTap: () => context.go('/profile'), ), ListTile( leading: const Icon(Icons.qr_code_scanner), title: Text(tr('ui.userfront.nav.qr_scan')), onTap: () => context.go('/scan'), ), const Divider(), ListTile( leading: const Icon(Icons.logout), title: Text(tr('ui.userfront.nav.logout')), onTap: _logout, ), ], ), ), const Padding( padding: EdgeInsets.only(bottom: 16), child: LanguageSelector(compact: true), ), ], ); } Widget _buildSectionTitle(String title, String subtitle) { return Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( title, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: _ink, ), ), const SizedBox(width: 12), Text(subtitle, style: TextStyle(fontSize: 13, color: Colors.grey[600])), ], ); } Widget _buildInfoChip(IconData icon, String label) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: _subtle, borderRadius: BorderRadius.circular(999), border: Border.all(color: _border), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 16, color: _ink), const SizedBox(width: 6), Text( label, style: const TextStyle( fontSize: 12, color: _ink, fontWeight: FontWeight.w600, ), ), ], ), ); } Widget _buildHeaderCard(UserProfile profile) { final name = profile.name.isEmpty ? tr('msg.userfront.profile.name_missing') : profile.name; final email = profile.email.isEmpty ? tr('msg.userfront.profile.email_missing') : profile.email; final department = profile.department.isEmpty ? tr('msg.userfront.profile.department_missing') : profile.department; return Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: _surface, borderRadius: BorderRadius.circular(16), border: Border.all(color: _border), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 10), blurRadius: 18, offset: const Offset(0, 8), ), ], ), child: Row( children: [ const CircleAvatar(radius: 32, child: Icon(Icons.person, size: 32)), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( tr('msg.userfront.profile.greeting', params: {'name': name}), style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: _ink, ), ), const SizedBox(height: 6), Text( email, style: TextStyle(color: Colors.grey[600], fontSize: 14), ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ _buildInfoChip( Icons.badge_outlined, tr('ui.userfront.profile.manage'), ), _buildInfoChip( Icons.apartment, profile.tenant?.name ?? department, ), ], ), ], ), ), ], ), ); } Widget _buildCard(Widget child) { return Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: _surface, borderRadius: BorderRadius.circular(16), border: Border.all(color: _border), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 8), blurRadius: 12, offset: const Offset(0, 6), ), ], ), child: child, ); } Widget _buildReadOnlyTile(String label, String value) { final displayValue = value.isEmpty ? '-' : value; return ListTile( contentPadding: EdgeInsets.zero, title: Text(label), subtitle: Text(displayValue), trailing: Text( tr('ui.common.read_only'), style: TextStyle(color: Colors.grey[500], fontSize: 12), ), ); } Widget _buildEditableTile({ required String field, required String label, required String value, required UserProfile profile, required bool isUpdating, required TextEditingController controller, }) { final isEditing = _editingField == field; final displayValue = value.isEmpty ? '-' : value; if (!isEditing) { return ListTile( contentPadding: EdgeInsets.zero, title: Text(label), subtitle: Text(displayValue), trailing: TextButton( onPressed: isUpdating ? null : () => _startEditing(field, profile), child: Text(tr('ui.common.edit')), ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(fontWeight: FontWeight.w600)), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: TextField( controller: controller, focusNode: field == 'name' ? _nameFocus : _departmentFocus, textInputAction: TextInputAction.done, onSubmitted: (_) => _autoSaveIfEditing(profile, field), decoration: InputDecoration( border: const OutlineInputBorder(), hintText: label, ), ), ), const SizedBox(width: 12), OutlinedButton( onPressed: isUpdating ? null : () => _cancelEditing(profile), child: Text(tr('ui.common.cancel')), ), ], ), ], ); } Widget _buildPhoneEditor(UserProfile profile, bool isUpdating) { final isEditing = _editingField == 'phone'; final displayValue = profile.phone.isEmpty ? '-' : profile.phone; if (!isEditing) { return ListTile( contentPadding: EdgeInsets.zero, title: Text(tr('ui.userfront.profile.phone.title')), subtitle: Text(displayValue), trailing: TextButton( onPressed: isUpdating ? null : () => _startEditing('phone', profile), child: Text(tr('ui.common.edit')), ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( tr('ui.userfront.profile.phone.title'), style: const TextStyle(fontWeight: FontWeight.w600), ), const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: TextField( controller: _phoneController, focusNode: _phoneFocus, keyboardType: TextInputType.phone, textInputAction: TextInputAction.done, onSubmitted: (_) => _autoSaveIfEditing(profile, 'phone'), decoration: InputDecoration( border: const OutlineInputBorder(), hintText: '01012345678', suffixIcon: _isPhoneVerified ? const Icon(Icons.check_circle, color: Colors.green) : null, ), enabled: !_isPhoneVerified, ), ), const SizedBox(width: 8), if (_isPhoneChanged && !_isPhoneVerified) ElevatedButton( onPressed: _isVerifying ? null : _sendCode, child: Text( _isCodeSent ? tr('ui.common.resend') : tr('ui.userfront.profile.phone.request_code'), ), ), const SizedBox(width: 8), OutlinedButton( onPressed: isUpdating ? null : () => _cancelEditing(profile), child: Text(tr('ui.common.cancel')), ), ], ), if (_isCodeSent && !_isPhoneVerified) ...[ const SizedBox(height: 12), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: TextField( controller: _codeController, focusNode: _phoneCodeFocus, keyboardType: TextInputType.number, textInputAction: TextInputAction.done, onSubmitted: (_) => _verifyCode(profile), decoration: InputDecoration( border: const OutlineInputBorder(), hintText: tr('ui.userfront.profile.phone.code_hint'), ), ), ), const SizedBox(width: 8), ElevatedButton( onPressed: _isVerifying ? null : () => _verifyCode(profile), child: Text(tr('ui.common.confirm')), ), ], ), ], if (_isPhoneChanged && !_isPhoneVerified) Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( tr('msg.userfront.profile.phone.verify_notice'), style: const TextStyle(color: Colors.orange, fontSize: 12), ), ), ], ); } Widget _buildPasswordSection() { return _buildCard( Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( tr('ui.userfront.profile.password.title'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700), ), const SizedBox(height: 8), Text( tr('msg.userfront.profile.password.subtitle'), style: const TextStyle(color: Color(0xFF6B7280)), ), const SizedBox(height: 16), TextField( controller: _currentPasswordController, obscureText: !_showCurrentPassword, decoration: InputDecoration( labelText: tr('ui.userfront.profile.password.current'), border: const OutlineInputBorder(), suffixIcon: IconButton( icon: Icon( _showCurrentPassword ? Icons.visibility_off : Icons.visibility, ), onPressed: () => setState(() { _showCurrentPassword = !_showCurrentPassword; }), ), ), ), const SizedBox(height: 12), TextField( controller: _newPasswordController, obscureText: !_showNewPassword, decoration: InputDecoration( labelText: tr('ui.userfront.profile.password.new'), border: const OutlineInputBorder(), suffixIcon: IconButton( icon: Icon( _showNewPassword ? Icons.visibility_off : Icons.visibility, ), onPressed: () => setState(() { _showNewPassword = !_showNewPassword; }), ), ), ), const SizedBox(height: 12), TextField( controller: _confirmPasswordController, obscureText: !_showConfirmPassword, decoration: InputDecoration( labelText: tr('ui.userfront.profile.password.confirm'), border: const OutlineInputBorder(), suffixIcon: IconButton( icon: Icon( _showConfirmPassword ? Icons.visibility_off : Icons.visibility, ), onPressed: () => setState(() { _showConfirmPassword = !_showConfirmPassword; }), ), ), ), if (_passwordError != null) ...[ const SizedBox(height: 12), Text(_passwordError!, style: const TextStyle(color: Colors.red)), ], if (_passwordSuccess != null) ...[ const SizedBox(height: 12), Text( _passwordSuccess!, style: const TextStyle(color: Colors.green), ), ], const SizedBox(height: 16), Row( children: [ ElevatedButton( onPressed: _isPasswordSaving ? null : _changePassword, child: _isPasswordSaving ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : Text(tr('ui.userfront.profile.password.change')), ), const SizedBox(width: 12), TextButton( onPressed: () => context.go('/recovery'), child: Text(tr('ui.userfront.profile.password.forgot')), ), ], ), ], ), ); } Widget _buildContent(UserProfile profile, bool isUpdating) { return RefreshIndicator( onRefresh: () => ref.read(profileProvider.notifier).loadProfile(), child: LayoutBuilder( builder: (context, constraints) { return Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 1200), child: ListView( padding: const EdgeInsets.all(24.0), children: [ _buildHeaderCard(profile), const SizedBox(height: 28), _buildSectionTitle( tr('ui.userfront.profile.section.basic'), tr('msg.userfront.profile.section.basic'), ), const SizedBox(height: 12), _buildCard( Column( children: [ _buildEditableTile( field: 'name', label: tr('ui.userfront.profile.field.name'), value: profile.name, profile: profile, isUpdating: isUpdating, controller: _nameController!, ), const Divider(height: 24), _buildReadOnlyTile( tr('ui.userfront.profile.field.email'), profile.email, ), const Divider(height: 24), _buildPhoneEditor(profile, isUpdating), ], ), ), const SizedBox(height: 28), _buildSectionTitle( tr('ui.userfront.profile.section.organization'), tr('msg.userfront.profile.section.organization'), ), const SizedBox(height: 12), _buildCard( Column( children: [ _buildEditableTile( field: 'department', label: tr('ui.userfront.profile.field.department'), value: profile.department, profile: profile, isUpdating: isUpdating, controller: _departmentController!, ), const Divider(height: 24), _buildReadOnlyTile( tr('ui.userfront.profile.field.affiliation'), profile.affiliationType, ), if (profile.tenant != null) ...[ const Divider(height: 24), _buildReadOnlyTile( tr('ui.userfront.profile.field.tenant'), profile.tenant!.name, ), ], if (profile.companyCode.isNotEmpty) ...[ const Divider(height: 24), _buildReadOnlyTile( tr('ui.userfront.profile.field.company_code'), profile.companyCode, ), ], ], ), ), const SizedBox(height: 28), _buildSectionTitle( tr('ui.userfront.profile.section.security'), tr('msg.userfront.profile.section.security'), ), const SizedBox(height: 12), _buildPasswordSection(), if (isUpdating || _isVerifying) ...[ const SizedBox(height: 24), const Center(child: CircularProgressIndicator()), ], ], ), ), ); }, ), ); } @override Widget build(BuildContext context) { final profileState = ref.watch(profileProvider); if (profileState.value != null) { _cachedProfile = profileState.value; } final profile = profileState.value ?? _cachedProfile; if (profile == null) { return Scaffold( appBar: AppBar(title: Text(tr('ui.userfront.nav.profile'))), body: profileState.isLoading ? const Center(child: CircularProgressIndicator()) : Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(tr('msg.userfront.profile.load_failed')), const SizedBox(height: 16), ElevatedButton( onPressed: () => ref.read(profileProvider.notifier).loadProfile(), child: Text(tr('ui.common.retry')), ), ], ), ), ); } _ensureControllers(profile); final isWide = MediaQuery.of(context).size.width >= sideMenuBreakpoint; final isUpdating = profileState.isLoading; return Scaffold( backgroundColor: _subtle, appBar: AppBar( title: Text( tr('ui.userfront.app_title'), style: const TextStyle(fontWeight: FontWeight.bold), ), elevation: 0, backgroundColor: _surface, foregroundColor: Colors.black, actions: [ IconButton( icon: const Icon(Icons.home_outlined), tooltip: tr('ui.userfront.nav.dashboard'), onPressed: () => context.go(buildLocalizedHomePath(Uri.base)), ), IconButton( icon: const Icon(Icons.qr_code_scanner), tooltip: tr('ui.userfront.nav.qr_scan'), onPressed: () => context.push('/scan'), ), IconButton( icon: const Icon(Icons.logout), tooltip: tr('ui.userfront.nav.logout'), onPressed: _logout, ), ], ), drawer: isWide ? null : Drawer(child: _buildSideMenu(context)), body: Row( children: [ if (isWide) SizedBox(width: 240, child: _buildSideMenu(context)), Expanded(child: _buildContent(profile, isUpdating)), ], ), ); } }