import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:geosector_app/app.dart'; import 'package:geosector_app/core/services/app_info_service.dart'; import 'package:geosector_app/core/services/current_amicale_service.dart'; import 'package:geosector_app/presentation/widgets/connectivity_indicator.dart'; import 'package:geosector_app/presentation/widgets/user_form_dialog.dart'; import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart'; import 'package:geosector_app/core/utils/api_exception.dart'; import 'package:geosector_app/core/services/theme_service.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:go_router/go_router.dart'; /// AppBar personnalisée pour les tableaux de bord class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { /// Le titre principal de l'AppBar (généralement le nom de l'application) final String title; /// Le titre de la page actuelle (optionnel) final String? pageTitle; /// Indique si le bouton "Nouveau passage" doit être affiché final bool showNewPassageButton; /// Callback appelé lorsque le bouton "Nouveau passage" est pressé final VoidCallback? onNewPassagePressed; /// Indique si l'utilisateur est un administrateur final bool isAdmin; /// Callback appelé lorsque le bouton de déconnexion est pressé final VoidCallback? onLogoutPressed; const DashboardAppBar({ super.key, required this.title, this.pageTitle, this.showNewPassageButton = true, this.onNewPassagePressed, this.isAdmin = false, this.onLogoutPressed, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); // Vérifier si le logo de l'amicale est présent pour ajuster la largeur du leading final amicale = CurrentAmicaleService.instance.currentAmicale; final hasAmicaleLogo = amicale?.logoBase64 != null && amicale!.logoBase64!.isNotEmpty; return Column( mainAxisSize: MainAxisSize.min, children: [ AppBar( title: _buildTitle(context), backgroundColor: theme.colorScheme.primary, foregroundColor: theme.colorScheme.onPrimary, elevation: 4, leading: _buildLogo(), // Ajuster la largeur du leading si le logo de l'amicale est présent leadingWidth: hasAmicaleLogo ? 110 : 56, // 56 par défaut, 110 pour 2 logos + espacement actions: _buildActions(context), ), // Bordure colorée selon le rôle Container( height: 3, color: isAdmin ? Colors.red : Colors.green, ), ], ); } /// Construction du logo dans l'AppBar Widget _buildLogo() { final amicale = CurrentAmicaleService.instance.currentAmicale; final logoBase64 = amicale?.logoBase64; return Padding( padding: const EdgeInsets.all(8.0), child: Row( mainAxisSize: MainAxisSize.min, children: [ Image.asset( 'assets/images/logo-geosector-1024.png', width: 40, height: 40, ), // Afficher le logo de l'amicale s'il est disponible if (logoBase64 != null && logoBase64.isNotEmpty) ...[ const SizedBox(width: 8), _buildAmicaleLogo(logoBase64), ], ], ), ); } /// Construction du logo de l'amicale à partir du Base64 Widget _buildAmicaleLogo(String logoBase64) { try { // Le logoBase64 peut être au format "data:image/png;base64,..." ou juste la chaîne base64 String base64String = logoBase64; if (logoBase64.contains('base64,')) { base64String = logoBase64.split('base64,').last; } final decodedBytes = base64Decode(base64String); return Container( width: 40, height: 40, decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), color: Colors.white, ), child: ClipRRect( borderRadius: BorderRadius.circular(4), child: Image.memory( decodedBytes, width: 40, height: 40, fit: BoxFit.contain, errorBuilder: (context, error, stackTrace) { // En cas d'erreur de décodage, ne rien afficher debugPrint('Erreur lors du chargement du logo amicale: $error'); return const SizedBox.shrink(); }, ), ), ); } catch (e) { // En cas d'erreur, ne rien afficher debugPrint('Erreur lors du décodage du logo amicale: $e'); return const SizedBox.shrink(); } } /// Construction des actions de l'AppBar List _buildActions(BuildContext context) { final theme = Theme.of(context); final List actions = []; // Ajouter l'indicateur de connectivité actions.add( const Padding( padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), child: ConnectivityIndicator( showErrorMessage: false, showConnectionType: true, ), ), ); actions.add(const SizedBox(width: 8)); // Ajouter la version de l'application actions.add( Text( "v${AppInfoService.version}", style: const TextStyle( fontSize: 12, color: Colors.white70, ), ), ); actions.add(const SizedBox(width: 8)); // Ajouter le bouton "Nouveau passage" seulement si l'utilisateur n'est pas admin if (!isAdmin) { actions.add( TextButton.icon( icon: const Icon(Icons.add_location_alt, color: Colors.white), label: const Text('Nouveau passage', style: TextStyle(color: Colors.white)), onPressed: () { showDialog( context: context, barrierDismissible: false, builder: (dialogContext) => PassageFormDialog( title: 'Nouveau passage', passageRepository: passageRepository, userRepository: userRepository, operationRepository: operationRepository, onSuccess: () { // Callback après création du passage if (onNewPassagePressed != null) { onNewPassagePressed!(); } }, ), ); }, style: TextButton.styleFrom( backgroundColor: Color(AppKeys.typesPassages[1]!['couleur1'] as int), // Vert des passages effectués padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ), ); actions.add(const SizedBox(width: 8)); } // Ajouter le sélecteur de thème avec confirmation (désactivé temporairement) // TODO: Réactiver quand le thème sombre sera corrigé // actions.add( // _buildThemeSwitcherWithConfirmation(context), // ); // // actions.add(const SizedBox(width: 8)); // Ajouter le bouton "Mon compte" actions.add( IconButton( icon: const Icon(Icons.person), tooltip: 'Mon compte', onPressed: () { // Afficher la boîte de dialogue UserForm avec l'utilisateur actuel final user = userRepository.currentUser; if (user != null) { showDialog( context: context, builder: (context) => UserFormDialog( title: 'Mon compte', user: user, readOnly: false, showRoleSelector: false, onSubmit: (updatedUser, {String? password}) async { try { // Sauvegarder les modifications de l'utilisateur // Note: password est ignoré ici car l'utilisateur normal ne peut pas changer son mot de passe await userRepository.updateUser(updatedUser); if (context.mounted) { Navigator.of(context).pop(); ApiException.showSuccess(context, 'Profil mis à jour'); } } catch (e) { debugPrint('❌ Erreur mise à jour de votre profil: $e'); ApiException.showError(context, e); } }, ), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Erreur: Utilisateur non trouvé'), backgroundColor: theme.colorScheme.error, ), ); } }, ), ); actions.add(const SizedBox(width: 8)); // Ajouter le bouton de déconnexion actions.add( IconButton( icon: const Icon(Icons.logout), tooltip: 'Déconnexion', style: IconButton.styleFrom( foregroundColor: Colors.red, ), onPressed: onLogoutPressed ?? () { showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('Déconnexion'), content: const Text('Voulez-vous vraiment vous déconnecter ?'), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Annuler'), ), TextButton( onPressed: () async { // Fermer la dialog d'abord Navigator.of(dialogContext).pop(); // Utiliser le context original de l'AppBar pour la navigation final success = await userRepository.logout(context); // Vérification supplémentaire et navigation forcée si nécessaire if (success && context.mounted) { // Attendre un court instant pour que les changements d'état se propagent await Future.delayed( const Duration(milliseconds: 100)); // Navigation vers splash avec paramètres pour redirection automatique final loginType = isAdmin ? 'admin' : 'user'; context.go('/?action=login&type=$loginType'); } }, child: const Text('Déconnexion'), ), ], ), ); }, ), ); actions.add(const SizedBox(width: 8)); // Espacement à droite return actions; } /// Construction du titre de l'AppBar Widget _buildTitle(BuildContext context) { // Si aucun titre de page n'est fourni, afficher simplement le titre principal if (pageTitle == null) { return Text(title); } // Utiliser LayoutBuilder pour détecter la largeur disponible return LayoutBuilder( builder: (context, constraints) { // Déterminer si on est sur mobile ou écran étroit final isNarrowScreen = constraints.maxWidth < 600; final isMobilePlatform = Theme.of(context).platform == TargetPlatform.android || Theme.of(context).platform == TargetPlatform.iOS; // Sur mobile ou écrans étroits, afficher seulement le titre principal if (isNarrowScreen || isMobilePlatform) { return Text(title); } // Sur écrans larges (web desktop), afficher le titre de la page ou le titre principal // Pour les admins, on affiche directement le titre de la page sans préfixe return Text(pageTitle!); }, ); } /// Construction du sélecteur de thème avec confirmation Widget _buildThemeSwitcherWithConfirmation(BuildContext context) { return IconButton( icon: Icon(ThemeService.instance.themeModeIcon), tooltip: 'Changer le thème (${ThemeService.instance.themeModeDescription})', onPressed: () async { final themeService = ThemeService.instance; final currentTheme = themeService.themeModeDescription; // Déterminer le prochain thème String nextTheme; switch (themeService.themeMode) { case ThemeMode.light: nextTheme = 'Sombre'; break; case ThemeMode.dark: nextTheme = 'Clair'; break; case ThemeMode.system: nextTheme = themeService.isSystemDark ? 'Clair' : 'Sombre'; break; } // Afficher la confirmation final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Row( children: [ Icon(Icons.palette_outlined), SizedBox(width: 8), Text('Changement de thème'), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Vous êtes actuellement sur le thème :'), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Theme.of(context) .colorScheme .primaryContainer .withOpacity(0.3), borderRadius: BorderRadius.circular(8), border: Border.all( color: Theme.of(context) .colorScheme .primary .withOpacity(0.3), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( themeService.themeModeIcon, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 8), Text( currentTheme, style: const TextStyle(fontWeight: FontWeight.bold), ), ], ), ), const SizedBox(height: 16), Text('Voulez-vous passer au thème $nextTheme ?'), const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of(context) .colorScheme .errorContainer .withOpacity(0.3), borderRadius: BorderRadius.circular(6), ), child: const Row( children: [ Icon(Icons.warning_amber, size: 16), SizedBox(width: 8), Expanded( child: Text( 'Note: Vous devrez vous reconnecter après ce changement.', style: TextStyle(fontSize: 12), ), ), ], ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('Annuler'), ), ElevatedButton( onPressed: () => Navigator.of(dialogContext).pop(true), child: Text('Passer au thème $nextTheme'), ), ], ), ); // Si confirmé, changer le thème if (confirmed == true) { await themeService.toggleTheme(); // Déconnecter l'utilisateur if (context.mounted) { final success = await userRepository.logout(context); if (success && context.mounted) { await Future.delayed(const Duration(milliseconds: 100)); // Rediriger vers splash avec paramètres pour revenir au même type de login final loginType = isAdmin ? 'admin' : 'user'; context.go('/?action=login&type=$loginType'); } } } }, ); } @override Size get preferredSize => const Size.fromHeight(kToolbarHeight + 3); // +3 pour la bordure }