import 'dart:io' show Platform, Directory, File; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../../shared/design_system/unionflow_design_system.dart'; import '../../../../shared/widgets/core_card.dart'; import '../../../../shared/widgets/info_badge.dart'; import '../../../../shared/widgets/mini_avatar.dart'; import '../../../../core/l10n/locale_provider.dart'; import '../../../../core/theme/theme_provider.dart'; import '../../../authentication/presentation/bloc/auth_bloc.dart'; import '../../../members/data/models/membre_complete_model.dart'; import '../../../organizations/bloc/org_switcher_bloc.dart'; import '../../../organizations/presentation/pages/org_selector_page.dart'; import '../../../settings/presentation/pages/language_settings_page.dart'; import '../../../settings/presentation/pages/privacy_settings_page.dart'; import '../../../settings/presentation/pages/feedback_page.dart'; import '../widgets/kyc_status_widget.dart'; import '../bloc/profile_bloc.dart'; /// Page Mon Profil - UnionFlow Mobile /// /// Page complète de gestion du profil utilisateur avec informations personnelles, /// préférences, sécurité, et paramètres avancés. class ProfilePage extends StatefulWidget { const ProfilePage({super.key}); @override State createState() => _ProfilePageState(); } class _ProfilePageState extends State with TickerProviderStateMixin { late TabController _tabController; final _formKey = GlobalKey(); // Contrôleurs pour les champs de texte final _firstNameController = TextEditingController(); final _lastNameController = TextEditingController(); final _emailController = TextEditingController(); final _phoneController = TextEditingController(); final _addressController = TextEditingController(); final _cityController = TextEditingController(); final _postalCodeController = TextEditingController(); final _bioController = TextEditingController(); // État du profil bool _isEditing = false; bool _isLoading = false; String? _membreId; String _selectedLanguage = 'Français'; String _selectedTheme = 'Système'; bool _biometricEnabled = false; bool _twoFactorEnabled = false; // Préférences notifications (persistées via SharedPreferences) bool _notifPush = true; bool _notifEmail = false; bool _notifSon = true; // Infos app (chargées via package_info_plus) String _appVersion = '...'; String _platformInfo = '...'; // Tailles stockage (calculées au chargement) String _cacheSize = '...'; String _imagesSize = '...'; String _offlineSize = '...'; final List _themes = ['Système', 'Clair', 'Sombre']; static const _keyNotifPush = 'notif_push'; static const _keyNotifEmail = 'notif_email'; static const _keyNotifSon = 'notif_son'; @override void initState() { super.initState(); _tabController = TabController(length: 4, vsync: this); WidgetsBinding.instance.addPostFrameCallback((_) { _syncLanguageFromProvider(); _loadProfileFromAuth(); }); _loadPreferences(); _loadDeviceInfo(); _loadStorageInfo(); } @override void dispose() { _tabController.dispose(); _firstNameController.dispose(); _lastNameController.dispose(); _emailController.dispose(); _phoneController.dispose(); _addressController.dispose(); _cityController.dispose(); _postalCodeController.dispose(); _bioController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return BlocListener( listener: (context, state) { if (state is ProfileLoaded) { _populateFromMembre(state.membre); } if (state is ProfileUpdated) { _populateFromMembre(state.membre); _showSuccessSnackBar('Profil mis à jour avec succès'); setState(() { _isEditing = false; _isLoading = false; }); } if (state is ProfileError) { setState(() => _isLoading = false); _showErrorSnackBar(state.message); } if (state is ProfileUpdating) { setState(() => _isLoading = true); } if (state is PasswordChanged) { _showSuccessSnackBar('Mot de passe modifié avec succès'); } if (state is AccountDeleted) { _showSuccessSnackBar('Compte supprimé'); context.read().add(AuthLogoutRequested()); } }, child: Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: UFAppBar( title: 'MON PROFIL', backgroundColor: Theme.of(context).cardColor, foregroundColor: Theme.of(context).brightness == Brightness.dark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight, actions: [ IconButton( icon: Icon(_isEditing ? Icons.save_outlined : Icons.edit_outlined, size: 20), onPressed: () => _isEditing ? _saveProfile() : _startEditing(), ), ], ), body: ListView( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), children: [ _buildHeader(), const SizedBox(height: 8), _buildTabBar(), SizedBox( height: 600, // Ajuster selon contenu ou utiliser NestedScrollView child: TabBarView( controller: _tabController, children: [ _buildPersonalInfoTab(), _buildPreferencesTab(), _buildSecurityTab(), _buildAdvancedTab(), ], ), ), ], ), ), ); } /// Header avec données réelles du backend Widget _buildHeader() { return BlocBuilder( builder: (context, state) { final membre = (state is ProfileLoaded) ? state.membre : (state is ProfileUpdated ? state.membre : (state is ProfileUpdating ? state.membre : null)); final isLoading = state is ProfileLoading || state is ProfileInitial; final hasError = state is ProfileError || state is ProfileNotFound; // Badge : rôle de l'utilisateur (SUPER_ADMIN, ADMIN, etc.) + statut String badgeText; Color badgeColor; if (isLoading) { badgeText = 'CHARGEMENT...'; badgeColor = AppColors.textSecondaryLight; } else if (hasError) { badgeText = 'ERREUR CHARGEMENT'; badgeColor = AppColors.error; } else if (membre != null) { // Priorité au rôle s'il est disponible if (membre.role != null && membre.role!.isNotEmpty) { badgeText = membre.role!.replaceAll('_', ' '); badgeColor = AppColors.primaryGreen; } else { // Fallback sur le statut switch (membre.statut) { case StatutMembre.actif: badgeText = membre.cotisationAJour ? 'MEMBRE ACTIF' : 'COTISATION EN RETARD'; badgeColor = membre.cotisationAJour ? AppColors.success : AppColors.warning; break; case StatutMembre.inactif: badgeText = 'INACTIF'; badgeColor = AppColors.textSecondaryLight; break; case StatutMembre.suspendu: badgeText = 'SUSPENDU'; badgeColor = AppColors.error; break; case StatutMembre.enAttente: badgeText = 'EN ATTENTE'; badgeColor = AppColors.warning; break; } } } else { badgeText = 'CHARGEMENT...'; badgeColor = AppColors.textSecondaryLight; } // Ancienneté réelle String depuisValue = '—'; if (membre?.dateAdhesion != null) { final days = membre!.ancienneteJours ?? 0; if (days < 30) { depuisValue = '$days J'; } else if (days < 365) { depuisValue = '${(days / 30).floor()} MOIS'; } else { depuisValue = '${(days / 365).floor()} ANS'; } } // Événements réels final eventsValue = membre != null ? '${membre.nombreEvenementsParticipes}' : '—'; // Organisation réelle final orgValue = membre?.organisationNom != null ? '1' : '—'; return CoreCard( padding: const EdgeInsets.all(16), child: Column( children: [ Row( children: [ GestureDetector( onTap: _pickProfileImage, child: MiniAvatar( size: 64, fallbackText: membre != null ? membre.initiales : (_firstNameController.text.isNotEmpty ? _firstNameController.text[0].toUpperCase() : '?'), imageUrl: membre?.photo, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${_firstNameController.text} ${_lastNameController.text}'.trim().toUpperCase(), style: AppTypography.actionText.copyWith(fontSize: 14, fontWeight: FontWeight.bold), ), Text( _emailController.text.toLowerCase(), style: AppTypography.subtitleSmall.copyWith(fontSize: 11), ), const SizedBox(height: 8), if (isLoading) const SizedBox( height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2), ) else InfoBadge(text: badgeText, backgroundColor: badgeColor), if (hasError) GestureDetector( onTap: _loadProfileFromAuth, child: Text( 'Réessayer', style: AppTypography.subtitleSmall.copyWith( color: AppColors.primaryGreen, fontSize: 10, decoration: TextDecoration.underline, ), ), ), ], ), ), ], ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildStatItem('DEPUIS', depuisValue), _buildStatItem('EVENTS', eventsValue), _buildStatItem('ORG', orgValue), ], ), // Sélecteur d'organisation (multi-org) BlocBuilder( builder: (context, orgState) { if (orgState is! OrgSwitcherLoaded || orgState.organisations.length <= 1) { return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.only(top: 12), child: Center( child: OrgSwitcherBadge( activeOrg: orgState.active, onTap: () async { final selected = await showOrgSelector(context); if (selected != null && context.mounted) { context .read() .add(OrgSwitcherSelectRequested(selected)); } }, ), ), ); }, ), ], ), ); }, ); } Widget _buildStatItem(String label, String value) { return Column( children: [ Text(value, style: AppTypography.headerSmall.copyWith(fontSize: 14)), Text(label, style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)), ], ); } /// Carte de statistique Widget _buildStatCard(String label, String value, IconData icon) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(12), ), child: Column( children: [ Icon( icon, color: Colors.white, size: 20, ), const SizedBox(height: 4), Text( value, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white, ), ), Text( label, style: TextStyle( fontSize: 10, color: Colors.white.withOpacity(0.8), ), ), ], ), ); } /// Barre d'onglets Widget _buildTabBar() { final isDark = Theme.of(context).brightness == Brightness.dark; return Container( decoration: BoxDecoration( color: isDark ? AppColors.darkSurface : AppColors.lightSurface, borderRadius: BorderRadius.circular(8), border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5), ), child: TabBar( controller: _tabController, labelColor: AppColors.primaryGreen, unselectedLabelColor: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, indicatorColor: AppColors.primaryGreen, indicatorSize: TabBarIndicatorSize.label, labelStyle: AppTypography.actionText.copyWith(fontSize: 10, fontWeight: FontWeight.bold), tabs: const [ Tab(text: 'PERSO'), Tab(text: 'PRÉF'), Tab(text: 'SÉCU'), Tab(text: 'AVANCÉ'), ], ), ); } /// Onglet informations personnelles Widget _buildPersonalInfoTab() { return SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( children: [ const SizedBox(height: 8), // Section informations de base _buildInfoSection( 'Informations personnelles', 'Vos données personnelles et de contact', Icons.person, [ Row( children: [ Expanded( child: _buildTextField( controller: _firstNameController, label: 'Prénom', icon: Icons.person_outline, enabled: _isEditing, ), ), const SizedBox(width: 12), Expanded( child: _buildTextField( controller: _lastNameController, label: 'Nom', icon: Icons.person_outline, enabled: _isEditing, ), ), ], ), _buildTextField( controller: _emailController, label: 'Email', icon: Icons.email_outlined, enabled: _isEditing, keyboardType: TextInputType.emailAddress, ), _buildTextField( controller: _phoneController, label: 'Téléphone', icon: Icons.phone_outlined, enabled: _isEditing, keyboardType: TextInputType.phone, ), ], ), const SizedBox(height: 16), // Section adresse _buildInfoSection( 'Adresse', 'Votre adresse de résidence', Icons.location_on, [ _buildTextField( controller: _addressController, label: 'Adresse', icon: Icons.home_outlined, enabled: _isEditing, maxLines: 2, ), Row( children: [ Expanded( flex: 2, child: _buildTextField( controller: _cityController, label: 'Ville', icon: Icons.location_city_outlined, enabled: _isEditing, ), ), const SizedBox(width: 12), Expanded( child: _buildTextField( controller: _postalCodeController, label: 'Code postal', icon: Icons.markunread_mailbox_outlined, enabled: _isEditing, keyboardType: TextInputType.number, ), ), ], ), ], ), const SizedBox(height: 16), // Section biographie _buildInfoSection( 'À propos de moi', 'Partagez quelques mots sur vous', Icons.info, [ _buildTextField( controller: _bioController, label: 'Biographie', icon: Icons.edit_outlined, enabled: _isEditing, maxLines: 4, hintText: 'Parlez-nous de vous, vos intérêts, votre parcours...', ), ], ), const SizedBox(height: 16), // Boutons d'action _buildActionButtons(), const SizedBox(height: 80), ], ), ); } /// Section d'informations Widget _buildInfoSection( String title, String subtitle, IconData icon, List children, ) { return CoreCard( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(icon, color: AppColors.primaryGreen, size: 16), const SizedBox(width: 8), Text( title.toUpperCase(), style: AppTypography.actionText.copyWith(fontSize: 11, fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 16), ...children.map((child) => Padding( padding: const EdgeInsets.only(bottom: 12), child: child, )), ], ), ); } /// Champ de texte personnalisé Widget _buildTextField({ required TextEditingController controller, required String label, required IconData icon, bool enabled = true, TextInputType? keyboardType, int maxLines = 1, String? hintText, }) { final isDark = Theme.of(context).brightness == Brightness.dark; return TextFormField( controller: controller, enabled: enabled, keyboardType: keyboardType, maxLines: maxLines, style: AppTypography.bodyTextSmall.copyWith( fontSize: 12, color: isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight, ), decoration: InputDecoration( labelText: label.toUpperCase(), labelStyle: AppTypography.subtitleSmall.copyWith( fontSize: 9, fontWeight: FontWeight.bold, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), hintText: hintText, prefixIcon: Icon(icon, color: enabled ? AppColors.primaryGreen : AppColors.textSecondaryLight, size: 16), filled: true, fillColor: isDark ? AppColors.darkSurface : AppColors.lightSurface, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), border: OutlineInputBorder( borderRadius: BorderRadius.circular(4), borderSide: BorderSide(color: isDark ? AppColors.darkBorder : AppColors.lightBorder), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(4), borderSide: BorderSide(color: isDark ? AppColors.darkBorder : AppColors.lightBorder), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(4), borderSide: const BorderSide(color: AppColors.primaryGreen, width: 1), ), disabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(4), borderSide: BorderSide(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5), ), ), validator: (value) { if (label == 'Email' && value != null && value.isNotEmpty) { if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { return 'Email invalide'; } } return null; }, ); } /// Boutons d'action Widget _buildActionButtons() { final isDark = Theme.of(context).brightness == Brightness.dark; return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? AppColors.darkSurface : Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Row( children: [ if (_isEditing) ...[ Expanded( child: ElevatedButton.icon( onPressed: _isLoading ? null : _cancelEditing, style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[100], foregroundColor: Colors.grey[700], elevation: 0, padding: const EdgeInsets.symmetric(vertical: 12), ), icon: const Icon(Icons.cancel, size: 18), label: const Text('Annuler'), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( onPressed: _isLoading ? null : _saveProfile, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primaryGreen, foregroundColor: Colors.white, elevation: 0, padding: const EdgeInsets.symmetric(vertical: 12), ), icon: _isLoading ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : const Icon(Icons.save, size: 18), label: Text(_isLoading ? 'Sauvegarde...' : 'Sauvegarder'), ), ), ] else ...[ Expanded( child: ElevatedButton.icon( onPressed: _startEditing, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primaryGreen, foregroundColor: Colors.white, elevation: 0, padding: const EdgeInsets.symmetric(vertical: 12), ), icon: const Icon(Icons.edit, size: 18), label: const Text('Modifier le profil'), ), ), ], ], ), ); } /// Onglet préférences Widget _buildPreferencesTab() { return SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( children: [ const SizedBox(height: 16), // Langue et région _buildPreferenceSection( 'Langue et région', 'Personnaliser l\'affichage', Icons.language, [ Builder( builder: (ctx) { final isDark = Theme.of(ctx).brightness == Brightness.dark; final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight; final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight; return InkWell( onTap: () async { await Navigator.of(context).push( MaterialPageRoute(builder: (context) => const LanguageSettingsPage()), ); if (mounted) { final lp = context.read(); setState(() => _selectedLanguage = lp.currentLanguageName); } }, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Row( children: [ Icon(Icons.language, color: textSecondary, size: 22), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Langue', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary)), Text('Actuellement : $_selectedLanguage', style: AppTypography.subtitleSmall.copyWith(color: textSecondary)), ], ), ), Icon(Icons.chevron_right, color: textSecondary, size: 20), ], ), ), ); }, ), _buildDropdownPreference( 'Thème', 'Apparence de l\'application', _selectedTheme, _themes, (value) { if (value == null) return; setState(() => _selectedTheme = value); final mode = switch (value) { 'Clair' => ThemeMode.light, 'Sombre' => ThemeMode.dark, _ => ThemeMode.system, }; context.read().setMode(mode); }, ), ], ), const SizedBox(height: 16), // Notifications _buildPreferenceSection( 'Notifications', 'Gérer vos alertes', Icons.notifications, [ _buildSwitchPreference( 'Notifications push', 'Recevoir des notifications sur cet appareil', _notifPush, (value) async { setState(() => _notifPush = value); await _savePreference(_keyNotifPush, value); }, ), _buildSwitchPreference( 'Notifications email', 'Recevoir des emails de notification', _notifEmail, (value) async { setState(() => _notifEmail = value); await _savePreference(_keyNotifEmail, value); }, ), _buildSwitchPreference( 'Sons et vibrations', 'Alertes sonores et vibrations', _notifSon, (value) async { setState(() => _notifSon = value); await _savePreference(_keyNotifSon, value); }, ), ], ), const SizedBox(height: 16), // Confidentialité _buildPreferenceSection( 'Confidentialité', 'Contrôler vos données', Icons.privacy_tip, [ Builder( builder: (ctx) { final isDark = Theme.of(ctx).brightness == Brightness.dark; final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight; final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight; return InkWell( onTap: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => const PrivacySettingsPage()), ); }, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Row( children: [ Icon(Icons.privacy_tip, color: textSecondary, size: 22), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Gérer la confidentialité', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary)), Text('Visibilité, partage de données, suppression de compte', style: AppTypography.subtitleSmall.copyWith(color: textSecondary)), ], ), ), Icon(Icons.chevron_right, color: textSecondary, size: 20), ], ), ), ); }, ), ], ), const SizedBox(height: 80), ], ), ); } /// Onglet sécurité Widget _buildSecurityTab() { return BlocBuilder( builder: (context, state) { return SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( children: [ const SizedBox(height: 16), // KYC/LCB-FT Status (si membre chargé) if (state is ProfileLoaded) ...[ KycStatusWidget( niveauVigilance: state.membre.niveauVigilanceKyc, statutKyc: state.membre.statutKyc, dateVerification: state.membre.dateVerificationIdentite, ), const SizedBox(height: 16), ], // Authentification _buildSecuritySection( 'Authentification', 'Sécuriser votre compte', Icons.security, [ _buildSecurityItem( 'Changer le mot de passe', 'Dernière modification il y a 3 mois', Icons.lock_outline, () => _showChangePasswordDialog(), ), _buildSwitchPreference( 'Authentification biométrique', 'Utiliser l\'empreinte digitale ou Face ID', _biometricEnabled, (value) { setState(() => _biometricEnabled = value); _showSuccessSnackBar('Authentification biométrique ${value ? 'activée' : 'désactivée'}'); }, ), _buildSwitchPreference( 'Authentification à deux facteurs', 'Sécurité renforcée avec SMS ou app', _twoFactorEnabled, (value) { setState(() => _twoFactorEnabled = value); if (value) { _showTwoFactorSetupDialog(); } else { _showSuccessSnackBar('Authentification à deux facteurs désactivée'); } }, ), ], ), const SizedBox(height: 16), // Sessions actives _buildSecuritySection( 'Session active', 'Cet appareil', Icons.devices, [ _buildSessionItem( 'Cet appareil', '${kIsWeb ? "Web" : Platform.isAndroid ? "Android" : Platform.isIOS ? "iOS" : "Bureau"} • Maintenant', kIsWeb ? Icons.web : Platform.isAndroid ? Icons.smartphone : Platform.isIOS ? Icons.phone_iphone : Icons.computer, true, ), ], ), const SizedBox(height: 16), // Actions de sécurité _buildSecuritySection( 'Actions de sécurité', 'Gérer votre compte', Icons.warning, [ _buildActionItem( 'Télécharger mes données', 'Exporter toutes vos données personnelles', Icons.download, AppColors.primaryGreen, () => _exportUserData(), ), _buildActionItem( 'Déconnecter tous les appareils', 'Fermer toutes les sessions actives', Icons.logout, AppColors.warning, () => _logoutAllDevices(), ), _buildActionItem( 'Supprimer mon compte', 'Action irréversible - toutes les données seront perdues', Icons.delete_forever, Colors.red, () => _showDeleteAccountDialog(), ), ], ), const SizedBox(height: 80), ], ), ); }, ); } /// Onglet avancé Widget _buildAdvancedTab() { return SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( children: [ const SizedBox(height: 16), // Données et stockage _buildAdvancedSection( 'Données et stockage', 'Gérer l\'utilisation des données', Icons.storage, kIsWeb ? [ Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text( 'Gestion du stockage non disponible sur navigateur web.', style: AppTypography.subtitleSmall.copyWith(fontSize: 11), ), ), ] : [ _buildStorageItem('Cache de l\'application', _cacheSize, () => _clearCache()), _buildStorageItem('Images téléchargées', _imagesSize, () => _clearImages()), _buildStorageItem('Données hors ligne', _offlineSize, () => _clearOfflineData()), ], ), const SizedBox(height: 16), // Développeur _buildAdvancedSection( 'Options développeur', 'Paramètres avancés', Icons.code, [ _buildSwitchPreference( 'Mode développeur', 'Afficher les options de débogage', false, (value) => _showSuccessSnackBar('Mode développeur ${value ? 'activé' : 'désactivé'}'), ), _buildSwitchPreference( 'Logs détaillés', 'Enregistrer plus d\'informations de débogage', false, (value) => _showSuccessSnackBar('Logs détaillés ${value ? 'activés' : 'désactivés'}'), ), ], ), const SizedBox(height: 16), // Informations système _buildAdvancedSection( 'Informations système', 'Détails techniques', Icons.info, [ _buildInfoItem('Version de l\'app', _appVersion), _buildInfoItem('Plateforme', _platformInfo), ], ), const SizedBox(height: 16), // Feedback / Commentaires _buildAdvancedSection( 'Commentaires', 'Aidez-nous à améliorer l\'application', Icons.feedback, [ Builder( builder: (ctx) { final isDark = Theme.of(ctx).brightness == Brightness.dark; final textPrimary = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight; final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight; return InkWell( onTap: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => const FeedbackPage()), ); }, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Row( children: [ Icon(Icons.edit_note, color: textSecondary, size: 22), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Envoyer des commentaires', style: AppTypography.bodyTextSmall.copyWith(fontWeight: FontWeight.w600, color: textPrimary)), Text('Suggestions, bugs ou idées d\'amélioration', style: AppTypography.subtitleSmall.copyWith(color: textSecondary)), ], ), ), Icon(Icons.chevron_right, color: textSecondary, size: 20), ], ), ), ); }, ), ], ), const SizedBox(height: 80), ], ), ); } // Feedback dialog déplacé dans FeedbackPage // ==================== MÉTHODES DE CONSTRUCTION DES COMPOSANTS ==================== /// Section de préférence Widget _buildPreferenceSection( String title, String subtitle, IconData icon, List children, ) { return CoreCard( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(icon, color: AppColors.primaryGreen, size: 16), const SizedBox(width: 8), Text( title.toUpperCase(), style: AppTypography.actionText.copyWith(fontSize: 11, fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 16), ...children.map((child) => Padding( padding: const EdgeInsets.only(bottom: 12), child: child, )), ], ), ); } /// Section de sécurité Widget _buildSecuritySection( String title, String subtitle, IconData icon, List children, ) { return _buildPreferenceSection(title, subtitle, icon, children); } /// Section avancée Widget _buildAdvancedSection( String title, String subtitle, IconData icon, List children, ) { return _buildPreferenceSection(title, subtitle, icon, children); } /// Préférence avec dropdown Widget _buildDropdownPreference( String title, String subtitle, String value, List options, Function(String?) onChanged, ) { final isDark = Theme.of(context).brightness == Brightness.dark; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title.toUpperCase(), style: AppTypography.subtitleSmall.copyWith(fontSize: 9, fontWeight: FontWeight.bold), ), const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: isDark ? AppColors.darkSurface : AppColors.lightSurface, borderRadius: BorderRadius.circular(4), border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: value, isExpanded: true, onChanged: onChanged, icon: const Icon(Icons.arrow_drop_down, color: AppColors.primaryGreen, size: 18), style: AppTypography.bodyTextSmall.copyWith(fontSize: 12), items: options.map((option) { return DropdownMenuItem( value: option, child: Text(option), ); }).toList(), ), ), ), ], ); } /// Préférence avec switch Widget _buildSwitchPreference( String title, String subtitle, bool value, Function(bool) onChanged, ) { final isDark = Theme.of(context).brightness == Brightness.dark; return Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title.toUpperCase(), style: AppTypography.subtitleSmall.copyWith(fontSize: 10, fontWeight: FontWeight.bold), ), Text( subtitle, style: AppTypography.bodyTextSmall.copyWith( fontSize: 10, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), ], ), ), Transform.scale( scale: 0.8, child: Switch( value: value, onChanged: onChanged, activeColor: AppColors.primaryGreen, ), ), ], ); } /// Élément de sécurité Widget _buildSecurityItem( String title, String subtitle, IconData icon, VoidCallback onTap, ) { final isDark = Theme.of(context).brightness == Brightness.dark; final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(4), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: isDark ? AppColors.darkSurface : AppColors.lightSurface, borderRadius: BorderRadius.circular(4), border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5), ), child: Row( children: [ Icon(icon, color: AppColors.primaryGreen, size: 16), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title.toUpperCase(), style: AppTypography.actionText.copyWith(fontSize: 11, fontWeight: FontWeight.bold), ), Text( subtitle, style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: textSecondary), ), ], ), ), Icon(Icons.arrow_forward_ios, color: textSecondary, size: 12), ], ), ), ); } /// Élément de session Widget _buildSessionItem( String title, String subtitle, IconData icon, bool isCurrentDevice, ) { final isDark = Theme.of(context).brightness == Brightness.dark; final textSecondary = isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isCurrentDevice ? AppColors.primaryGreen.withOpacity(0.05) : (isDark ? AppColors.darkSurface : AppColors.lightSurface), borderRadius: BorderRadius.circular(4), border: Border.all( color: isCurrentDevice ? AppColors.primaryGreen.withOpacity(0.3) : (isDark ? AppColors.darkBorder : AppColors.lightBorder), width: 0.5, ), ), child: Row( children: [ Icon( icon, color: isCurrentDevice ? AppColors.primaryGreen : textSecondary, size: 16, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( title.toUpperCase(), style: AppTypography.actionText.copyWith(fontSize: 11, fontWeight: FontWeight.bold), ), if (isCurrentDevice) ...[ const SizedBox(width: 8), const InfoBadge(text: 'ACTUEL', backgroundColor: AppColors.primaryGreen), ], ], ), Text( subtitle, style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: textSecondary), ), ], ), ), ], ), ); } /// Élément d'action Widget _buildActionItem( String title, String subtitle, IconData icon, Color color, VoidCallback onTap, ) { final isDark = Theme.of(context).brightness == Brightness.dark; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(4), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: color.withOpacity(0.05), borderRadius: BorderRadius.circular(4), border: Border.all(color: color.withOpacity(0.1), width: 0.5), ), child: Row( children: [ Icon(icon, color: color, size: 16), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title.toUpperCase(), style: AppTypography.actionText.copyWith(fontSize: 11, fontWeight: FontWeight.bold, color: color), ), Text( subtitle, style: AppTypography.bodyTextSmall.copyWith( fontSize: 10, color: isDark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), ), ], ), ), Icon(Icons.arrow_forward_ios, color: color.withOpacity(0.5), size: 12), ], ), ), ); } /// Élément de stockage Widget _buildStorageItem(String title, String size, VoidCallback onTap) { final isDark = Theme.of(context).brightness == Brightness.dark; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(4), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: isDark ? AppColors.darkSurface : AppColors.lightSurface, borderRadius: BorderRadius.circular(4), border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5), ), child: Row( children: [ const Icon(Icons.folder_outlined, color: AppColors.primaryGreen, size: 16), const SizedBox(width: 12), Expanded( child: Text( title.toUpperCase(), style: AppTypography.actionText.copyWith(fontSize: 11, fontWeight: FontWeight.bold), ), ), Text( size, style: AppTypography.subtitleSmall.copyWith(fontSize: 10), ), const SizedBox(width: 8), const Icon(Icons.clear, color: AppColors.error, size: 14), ], ), ), ); } /// Élément d'information Widget _buildInfoItem(String title, String value) { final isDark = Theme.of(context).brightness == Brightness.dark; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: isDark ? AppColors.darkSurface : AppColors.lightSurface, borderRadius: BorderRadius.circular(4), border: Border.all(color: isDark ? AppColors.darkBorder : AppColors.lightBorder, width: 0.5), ), child: Row( children: [ Expanded( child: Text( title.toUpperCase(), style: AppTypography.subtitleSmall.copyWith(fontSize: 9, fontWeight: FontWeight.bold), ), ), Text( value, style: AppTypography.bodyTextSmall.copyWith(fontSize: 11, fontWeight: FontWeight.bold), ), ], ), ); } // ==================== MÉTHODES D'ACTION ==================== /// Synchronise la langue et le thème affichés depuis leurs providers void _syncLanguageFromProvider() { if (!mounted) return; final lp = context.read(); final tp = context.read(); setState(() { _selectedLanguage = lp.currentLanguageName; _selectedTheme = switch (tp.mode) { ThemeMode.light => 'Clair', ThemeMode.dark => 'Sombre', _ => 'Système', }; }); } // Confidentialité gérée dans PrivacySettingsPage /// Charger le profil utilisateur /// Charge le profil depuis l'AuthBloc et déclenche la récupération backend void _loadProfileFromAuth() { final authState = context.read().state; if (authState is AuthAuthenticated) { // Pré-remplir avec les données Keycloak disponibles immédiatement setState(() { _firstNameController.text = authState.user.firstName; _lastNameController.text = authState.user.lastName; _emailController.text = authState.user.email; if (authState.user.phone != null) { _phoneController.text = authState.user.phone!; } }); // Charger le profil complet depuis le backend context.read().add(const LoadMe()); } } /// Peuple les champs depuis un membre récupéré du backend void _populateFromMembre(dynamic membre) { setState(() { _membreId = membre.id?.toString(); _firstNameController.text = membre.prenom ?? ''; _lastNameController.text = membre.nom ?? ''; _emailController.text = membre.email ?? ''; _phoneController.text = membre.telephone ?? ''; _addressController.text = membre.adresse ?? ''; _cityController.text = membre.ville ?? ''; _postalCodeController.text = membre.codePostal ?? ''; _bioController.text = membre.notes ?? ''; }); } void _loadUserProfile() { _loadProfileFromAuth(); } /// Commencer l'édition void _startEditing() { setState(() { _isEditing = true; }); } /// Annuler l'édition void _cancelEditing() { setState(() { _isEditing = false; }); _loadUserProfile(); // Recharger les données originales } /// Sauvegarder le profil Future _saveProfile() async { if (!_formKey.currentState!.validate()) return; if (_membreId == null) { _showErrorSnackBar('Profil non chargé. Impossible de sauvegarder.'); return; } final blocState = context.read().state; if (blocState is! ProfileLoaded) return; final membreMisAJour = blocState.membre.copyWith( prenom: _firstNameController.text.trim(), nom: _lastNameController.text.trim(), email: _emailController.text.trim(), telephone: _phoneController.text.trim(), adresse: _addressController.text.trim(), ville: _cityController.text.trim(), codePostal: _postalCodeController.text.trim(), notes: _bioController.text.trim(), ); context.read().add( UpdateMyProfile(membreId: _membreId!, membre: membreMisAJour), ); } /// Choisir une image de profil Future _pickProfileImage() async { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Changer la photo de profil'), content: const Text('Cette fonctionnalité sera bientôt disponible !'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Fermer'), ), ], ), ); } /// Charger les préférences notifications depuis SharedPreferences Future _loadPreferences() async { final prefs = await SharedPreferences.getInstance(); if (mounted) { setState(() { _notifPush = prefs.getBool(_keyNotifPush) ?? true; _notifEmail = prefs.getBool(_keyNotifEmail) ?? false; _notifSon = prefs.getBool(_keyNotifSon) ?? true; }); } } /// Persiste une préférence booléenne Future _savePreference(String key, bool value) async { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(key, value); } /// Charge les informations réelles de l'application Future _loadDeviceInfo() async { final info = await PackageInfo.fromPlatform(); if (mounted) { setState(() { _appVersion = '${info.version} (Build ${info.buildNumber})'; final os = kIsWeb ? 'Web' : Platform.isAndroid ? 'Android' : Platform.isIOS ? 'iOS' : Platform.operatingSystem; _platformInfo = '$os · ${info.appName}'; }); } } /// Dialogue de changement de mot de passe (branché sur le backend Keycloak) void _showChangePasswordDialog() { final oldPassCtrl = TextEditingController(); final newPassCtrl = TextEditingController(); final confirmPassCtrl = TextEditingController(); showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Changer le mot de passe'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: oldPassCtrl, obscureText: true, decoration: const InputDecoration( labelText: 'Mot de passe actuel', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: newPassCtrl, obscureText: true, decoration: const InputDecoration( labelText: 'Nouveau mot de passe', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: confirmPassCtrl, obscureText: true, decoration: const InputDecoration( labelText: 'Confirmer le nouveau mot de passe', border: OutlineInputBorder(), ), ), ], ), actions: [ TextButton( onPressed: () { Navigator.of(ctx).pop(); oldPassCtrl.dispose(); newPassCtrl.dispose(); confirmPassCtrl.dispose(); }, child: const Text('Annuler'), ), ElevatedButton( onPressed: () { final oldPass = oldPassCtrl.text.trim(); final newPass = newPassCtrl.text.trim(); final confirmPass = confirmPassCtrl.text.trim(); if (oldPass.isEmpty || newPass.isEmpty || confirmPass.isEmpty) { _showErrorSnackBar('Veuillez remplir tous les champs'); return; } if (newPass != confirmPass) { _showErrorSnackBar('Les mots de passe ne correspondent pas'); return; } if (newPass.length < 8) { _showErrorSnackBar('Le mot de passe doit contenir au moins 8 caractères'); return; } if (_membreId == null) { _showErrorSnackBar('Profil non chargé'); return; } Navigator.of(ctx).pop(); context.read().add(ChangePassword( membreId: _membreId!, oldPassword: oldPass, newPassword: newPass, )); oldPassCtrl.dispose(); newPassCtrl.dispose(); confirmPassCtrl.dispose(); }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primaryGreen, foregroundColor: Colors.white, ), child: const Text('Modifier'), ), ], ), ); } /// 2FA : non disponible (Keycloak géré côté admin) void _showTwoFactorSetupDialog() { showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Authentification à deux facteurs'), content: const Text( 'La configuration 2FA est gérée par votre administrateur Keycloak. ' 'Contactez votre organisation pour activer cette fonctionnalité.', ), actions: [ TextButton( onPressed: () { Navigator.of(ctx).pop(); setState(() => _twoFactorEnabled = false); }, child: const Text('Fermer'), ), ], ), ); } /// Export RGPD : non disponible (pas d'endpoint backend) void _exportUserData() { showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Télécharger mes données'), content: const Text( 'Cette fonctionnalité sera disponible prochainement. ' 'Pour demander l\'export de vos données, contactez l\'administrateur de votre organisation.', ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Fermer'), ), ], ), ); } /// Déconnexion tous appareils : non disponible (pas d'endpoint backend) void _logoutAllDevices() { showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Déconnecter tous les appareils'), content: const Text( 'Cette fonctionnalité sera disponible prochainement. ' 'Vous pouvez révoquer vos sessions depuis l\'interface Keycloak de votre organisation.', ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Fermer'), ), ], ), ); } /// Suppression de compte (branché sur le backend) void _showDeleteAccountDialog() { showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Supprimer mon compte'), content: const Text( 'ATTENTION : Cette action est irréversible !\n\n' 'Toutes vos données seront définitivement supprimées :\n' '• Profil et informations personnelles\n' '• Historique des événements\n' '• Participations aux organisations\n' '• Tous les paramètres et préférences', ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Annuler'), ), ElevatedButton( onPressed: () { Navigator.of(ctx).pop(); _showFinalDeleteConfirmation(); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), child: const Text('Continuer'), ), ], ), ); } /// Confirmation finale — déclenche la suppression réelle sur le backend void _showFinalDeleteConfirmation() { final confirmCtrl = TextEditingController(); showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Confirmation finale'), content: Column( mainAxisSize: MainAxisSize.min, children: [ const Text('Tapez "SUPPRIMER" pour confirmer :'), const SizedBox(height: 16), TextField( controller: confirmCtrl, decoration: const InputDecoration( border: OutlineInputBorder(), hintText: 'SUPPRIMER', ), ), ], ), actions: [ TextButton( onPressed: () { Navigator.of(ctx).pop(); confirmCtrl.dispose(); }, child: const Text('Annuler'), ), ElevatedButton( onPressed: () { if (confirmCtrl.text.trim() != 'SUPPRIMER') { _showErrorSnackBar('Tapez exactement "SUPPRIMER" pour confirmer'); return; } if (_membreId == null) { _showErrorSnackBar('Profil non chargé'); return; } Navigator.of(ctx).pop(); context.read().add(DeleteAccount(_membreId!)); confirmCtrl.dispose(); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), child: const Text('SUPPRIMER DÉFINITIVEMENT'), ), ], ), ); } /// Vide le cache temporaire de l'application /// Calcule la taille d'un répertoire (récursif) en format lisible Future _dirSize(Directory dir) async { if (!dir.existsSync()) return '0 B'; int bytes = 0; try { await for (final entity in dir.list(recursive: true)) { if (entity is File) bytes += await entity.length(); } } catch (_) {} if (bytes == 0) return '0 B'; if (bytes < 1024) return '$bytes B'; if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; } /// Charge les tailles des répertoires de stockage Future _loadStorageInfo() async { if (kIsWeb) return; try { final tmp = await getTemporaryDirectory(); final docs = await getApplicationDocumentsDirectory(); final support = await getApplicationSupportDirectory(); final cacheSize = await _dirSize(tmp); final imagesSize = await _dirSize(Directory('${docs.path}/images')); final offlineSize = await _dirSize(Directory('${support.path}/offline')); if (mounted) { setState(() { _cacheSize = cacheSize; _imagesSize = imagesSize; _offlineSize = offlineSize; }); } } catch (_) { if (mounted) { setState(() { _cacheSize = 'N/A'; _imagesSize = 'N/A'; _offlineSize = 'N/A'; }); } } } Future _clearCache() async { if (kIsWeb) return; try { final dir = await getTemporaryDirectory(); if (dir.existsSync()) { dir.listSync().forEach((e) { try { e.deleteSync(recursive: true); } catch (_) {} }); } _showSuccessSnackBar('Cache vidé avec succès'); } catch (_) { _showErrorSnackBar('Erreur lors du vidage du cache'); } finally { _loadStorageInfo(); } } /// Vide le répertoire des images téléchargées Future _clearImages() async { if (kIsWeb) return; try { final dir = await getApplicationDocumentsDirectory(); final imgDir = Directory('${dir.path}/images'); if (imgDir.existsSync()) { imgDir.listSync().forEach((e) { try { e.deleteSync(recursive: true); } catch (_) {} }); } _showSuccessSnackBar('Images supprimées'); } catch (_) { _showErrorSnackBar('Erreur lors de la suppression des images'); } finally { _loadStorageInfo(); } } /// Vide les données hors ligne Future _clearOfflineData() async { if (kIsWeb) return; try { final dir = await getApplicationSupportDirectory(); final offlineDir = Directory('${dir.path}/offline'); if (offlineDir.existsSync()) { offlineDir.listSync().forEach((e) { try { e.deleteSync(recursive: true); } catch (_) {} }); } _showSuccessSnackBar('Données hors ligne supprimées'); } catch (_) { _showErrorSnackBar('Erreur lors de la suppression des données hors ligne'); } finally { _loadStorageInfo(); } } /// Afficher un message de succès void _showSuccessSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( message.toUpperCase(), style: AppTypography.actionText.copyWith(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold), ), backgroundColor: AppColors.success, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), ), ); } void _showErrorSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( message.toUpperCase(), style: AppTypography.actionText.copyWith(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold), ), backgroundColor: AppColors.error, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), ), ); } }