import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:geosector_app/core/data/models/operation_model.dart'; import 'package:geosector_app/core/data/models/passage_model.dart'; import 'package:geosector_app/core/repositories/operation_repository.dart'; import 'package:geosector_app/core/repositories/user_repository.dart'; import 'package:geosector_app/core/utils/api_exception.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/presentation/widgets/operation_form_dialog.dart'; /// Page d'administration des opérations annuelles /// Cette page est intégrée dans le tableau de bord administrateur /// FOND TRANSPARENT - le fond dégradé est géré par AdminDashboardPage class AdminOperationsPage extends StatefulWidget { final OperationRepository operationRepository; final UserRepository userRepository; const AdminOperationsPage({ super.key, required this.operationRepository, required this.userRepository, }); @override State createState() => _AdminOperationsPageState(); } class _AdminOperationsPageState extends State { late int? _userAmicaleId; @override void initState() { super.initState(); _userAmicaleId = widget.userRepository.getCurrentUser()?.fkEntite; debugPrint('🔧 AdminOperationsPage initialisée - UserAmicaleId: $_userAmicaleId'); } void _showCreateOperationDialog() { showDialog( context: context, barrierDismissible: false, builder: (dialogContext) => OperationFormDialog( title: 'Créer une nouvelle opération', operationRepository: widget.operationRepository, userRepository: widget.userRepository, onSuccess: () { // Simple callback pour rafraîchir l'interface if (mounted) { setState(() {}); } }, ), ); } void _showEditOperationDialog(OperationModel op) { showDialog( context: context, barrierDismissible: false, builder: (dialogContext) => OperationFormDialog( title: op.isActive ? 'Modifier l\'opération active : ${op.name}' : 'Modifier l\'opération : ${op.name}', operation: op, operationRepository: widget.operationRepository, userRepository: widget.userRepository, onSuccess: () { // Simple callback pour rafraîchir l'interface if (mounted) { setState(() {}); } }, ), ); } /// Récupère les passages réalisés (fkType != 2) pour une opération int _getCompletedPassagesCount(int operationId) { try { final passagesBox = Hive.box(AppKeys.passagesBoxName); final completedPassages = passagesBox.values.where((passage) => passage.fkOperation == operationId && passage.fkType != 2).length; debugPrint('🔍 Passages réalisés pour opération $operationId: $completedPassages'); return completedPassages; } catch (e) { debugPrint('❌ Erreur lors du comptage des passages: $e'); return 0; } } void _handleDelete(OperationModel op, List operations) async { final currentUser = widget.userRepository.getCurrentUser(); if (currentUser == null) { ApiException.showError(context, Exception("Utilisateur non connecté")); return; } // Vérifier qu'il reste au moins une opération if (operations.length <= 1) { ApiException.showError(context, Exception("Impossible de supprimer la dernière opération")); return; } // Cas 1: Opération inactive - Suppression simple pour role > 1 if (!op.isActive && currentUser.role > 1) { final confirmed = await _showSimpleDeleteDialog(op); if (confirmed == true) { await _performSimpleDelete(op); } return; } // Cas 2: Opération active avec role = 2 - Vérification des passages if (op.isActive && currentUser.role == 2) { final completedPassagesCount = _getCompletedPassagesCount(op.id); if (completedPassagesCount > 0) { // Il y a des passages réalisés - Dialog d'avertissement avec confirmation par nom final confirmed = await _showActiveDeleteWithPassagesDialog(op, completedPassagesCount); if (confirmed == true) { await _performActiveDelete(op); } } else { // Pas de passages réalisés - Suppression simple final confirmed = await _showActiveDeleteDialog(op); if (confirmed == true) { await _performActiveDelete(op); } } return; } // Cas 3: Role > 2 - Suppression autorisée sans restrictions if (currentUser.role > 2) { final confirmed = await _showSimpleDeleteDialog(op); if (confirmed == true) { if (op.isActive) { await _performActiveDelete(op); } else { await _performSimpleDelete(op); } } return; } // Cas par défaut - Pas d'autorisation ApiException.showError(context, Exception("Vous n'avez pas les droits pour supprimer cette opération")); } /// Dialog simple pour suppression d'opération inactive Future _showSimpleDeleteDialog(OperationModel op) { return showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Row( children: [ Icon(Icons.warning, color: Colors.orange), SizedBox(width: 8), Text("Confirmer la suppression"), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Voulez-vous supprimer l'opération \"${op.name}\" ?"), const SizedBox(height: 8), const Text( "Cette action est définitive.", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text("Annuler"), ), ElevatedButton( onPressed: () => Navigator.of(dialogContext).pop(true), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), child: const Text("Supprimer"), ), ], ), ); } /// Dialog pour suppression d'opération active sans passages Future _showActiveDeleteDialog(OperationModel op) { return showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Row( children: [ Icon(Icons.warning, color: Colors.orange), SizedBox(width: 8), Text("Supprimer l'opération active"), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Voulez-vous supprimer l'opération active \"${op.name}\" ?"), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue.shade200), ), child: const Row( children: [ Icon(Icons.info, color: Colors.blue, size: 20), SizedBox(width: 8), Expanded( child: Text( "Votre dernière opération inactive sera automatiquement réactivée.", style: TextStyle(color: Colors.blue), ), ), ], ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text("Annuler"), ), ElevatedButton( onPressed: () => Navigator.of(dialogContext).pop(true), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), child: const Text("Supprimer"), ), ], ), ); } /// Dialog pour suppression d'opération active avec passages réalisés Future _showActiveDeleteWithPassagesDialog(OperationModel op, int passagesCount) { final TextEditingController nameController = TextEditingController(); return showDialog( context: context, builder: (dialogContext) => StatefulBuilder( builder: (context, setState) => AlertDialog( title: const Row( children: [ Icon(Icons.error, color: Colors.red), SizedBox(width: 8), Text("ATTENTION - Passages réalisés"), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.warning, color: Colors.red, size: 20), const SizedBox(width: 8), Text( "$passagesCount passage(s) réalisé(s) trouvé(s)", style: const TextStyle( fontWeight: FontWeight.bold, color: Colors.red, ), ), ], ), const SizedBox(height: 8), const Text( "La suppression de cette opération active supprimera définitivement tous les passages réalisés !", style: TextStyle(color: Colors.red), ), ], ), ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue.shade200), ), child: const Row( children: [ Icon(Icons.info, color: Colors.blue, size: 20), SizedBox(width: 8), Expanded( child: Text( "Votre dernière opération inactive sera automatiquement réactivée.", style: TextStyle(color: Colors.blue), ), ), ], ), ), const SizedBox(height: 16), const Text( "Pour confirmer, saisissez le nom exact de l'opération :", style: TextStyle(fontWeight: FontWeight.w600), ), const SizedBox(height: 8), TextField( controller: nameController, decoration: InputDecoration( hintText: op.name, border: const OutlineInputBorder(), isDense: true, ), onChanged: (value) => setState(() {}), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text("Annuler"), ), ElevatedButton( onPressed: nameController.text.trim() == op.name.trim() ? () => Navigator.of(dialogContext).pop(true) : null, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), child: const Text("Supprimer définitivement"), ), ], ), ), ); } /// Suppression simple d'opération inactive Future _performSimpleDelete(OperationModel op) async { try { final success = await widget.operationRepository.deleteOperationViaApi(op.id); if (success && mounted) { ApiException.showSuccess(context, "Opération supprimée avec succès"); setState(() {}); } else { throw Exception("Erreur lors de la suppression"); } } catch (e) { if (mounted) { ApiException.showError(context, e); } } } /// Suppression d'opération active (avec réactivation automatique) Future _performActiveDelete(OperationModel op) async { try { final success = await widget.operationRepository.deleteActiveOperationViaApi(op.id); if (success && mounted) { ApiException.showSuccess(context, "Opération active supprimée avec succès. L'opération précédente a été réactivée."); setState(() {}); } else { throw Exception("Erreur lors de la suppression"); } } catch (e) { if (mounted) { ApiException.showError(context, e); } } } void _handleExport(OperationModel operation) async { try { // Afficher un indicateur de chargement ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ), const SizedBox(width: 16), Text("Export Excel de l'opération \"${operation.name}\" en cours..."), ], ), duration: const Duration(seconds: 10), // Plus long pour le téléchargement backgroundColor: Colors.blue, ), ); // Appeler l'export via le repository await widget.operationRepository.exportOperationToExcel(operation.id, operation.name); // Masquer le SnackBar de chargement et afficher le succès if (mounted) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ApiException.showSuccess(context, "Export Excel de l'opération \"${operation.name}\" terminé avec succès !"); } } catch (e) { // Masquer le SnackBar de chargement et afficher l'erreur if (mounted) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ApiException.showError(context, e); } } } String _formatDate(DateTime date) { return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}'; } Widget _buildOperationsTable(List operations) { final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // En-tête du tableau _buildTableHeader(theme), // Corps du tableau Container( decoration: BoxDecoration( color: theme.colorScheme.surface, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8), ), border: Border.all( color: theme.colorScheme.primary.withValues(alpha: 0.1), width: 1, ), ), child: ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: operations.length, itemBuilder: (context, index) { final operation = operations[index]; return _buildOperationRow(operation, index % 2 == 1, theme, operations); }, ), ), ], ); } Widget _buildTableHeader(ThemeData theme) { final textStyle = theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.primary, ); return Container( decoration: BoxDecoration( color: theme.colorScheme.primary.withValues(alpha: 0.1), border: Border( bottom: BorderSide( color: theme.dividerColor.withValues(alpha: 0.3), width: 1, ), ), ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 12.0), child: Row( children: [ // Colonne ID Expanded( flex: 1, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text('ID', style: textStyle, overflow: TextOverflow.ellipsis), ), ), // Colonne Nom Expanded( flex: 4, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text('Nom de l\'opération', style: textStyle, overflow: TextOverflow.ellipsis), ), ), // Colonne Date début Expanded( flex: 2, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text('Date début', style: textStyle, overflow: TextOverflow.ellipsis), ), ), // Colonne Date fin Expanded( flex: 2, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text('Date fin', style: textStyle, overflow: TextOverflow.ellipsis), ), ), // Colonne Statut Expanded( flex: 2, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text('Statut', style: textStyle, overflow: TextOverflow.ellipsis), ), ), // Colonne Actions Expanded( flex: 2, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text('Actions', style: textStyle, overflow: TextOverflow.ellipsis), ), ), ], ), ), ); } Widget _buildOperationRow(OperationModel operation, bool isAlternate, ThemeData theme, List allOperations) { final textStyle = theme.textTheme.bodyMedium; final backgroundColor = isAlternate ? theme.colorScheme.surface : theme.colorScheme.surface; final canDelete = allOperations.length > 1; // Peut supprimer seulement s'il y a plus d'une opération return InkWell( onTap: operation.isActive ? () => _showEditOperationDialog(operation) : null, hoverColor: operation.isActive ? theme.colorScheme.primary.withValues(alpha: 0.05) : null, child: Container( decoration: BoxDecoration( color: backgroundColor, border: Border( bottom: BorderSide( color: theme.dividerColor.withValues(alpha: 0.3), width: 1, ), ), ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 12.0), child: Row( children: [ // Colonne ID Expanded( flex: 1, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( operation.id.toString(), style: textStyle, overflow: TextOverflow.ellipsis, ), ), ), // Colonne Nom Expanded( flex: 4, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Row( children: [ if (operation.isActive) ...[ Icon( Icons.edit_outlined, size: 16, color: theme.colorScheme.primary.withValues(alpha: 0.6), ), const SizedBox(width: 4), ], Expanded( child: Text( operation.name, style: textStyle?.copyWith( color: operation.isActive ? theme.colorScheme.primary : textStyle.color, fontWeight: operation.isActive ? FontWeight.w600 : textStyle.fontWeight, ), overflow: TextOverflow.ellipsis, ), ), ], ), ), ), // Colonne Date début Expanded( flex: 2, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( _formatDate(operation.dateDebut), style: textStyle, overflow: TextOverflow.ellipsis, ), ), ), // Colonne Date fin Expanded( flex: 2, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( _formatDate(operation.dateFin), style: textStyle, overflow: TextOverflow.ellipsis, ), ), ), // Colonne Statut Expanded( flex: 2, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), decoration: BoxDecoration( color: operation.isActive ? Colors.green : Colors.red, borderRadius: BorderRadius.circular(12.0), ), child: Text( operation.isActive ? 'Active' : 'Inactive', style: theme.textTheme.bodySmall?.copyWith( color: Colors.white, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, ), ), ), ), // Colonne Actions Expanded( flex: 2, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ // Bouton Delete - Affiché seulement s'il y a plus d'une opération if (canDelete) IconButton( icon: Icon( Icons.delete_forever, color: theme.colorScheme.error, size: 20, ), tooltip: 'Supprimer', onPressed: () => _handleDelete(operation, allOperations), constraints: const BoxConstraints( minWidth: 36, minHeight: 36, ), padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, ), // Bouton Export IconButton( icon: Icon( Icons.download, color: theme.colorScheme.secondary, size: 20, ), tooltip: 'Exporter', onPressed: () => _handleExport(operation), constraints: const BoxConstraints( minWidth: 36, minHeight: 36, ), padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, ), ], ), ), ), ], ), ), ), ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); debugPrint('🎨 AdminOperationsPage.build() appelée'); return Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Titre de la page Text( 'Gestion des opérations annuelles', style: theme.textTheme.headlineMedium?.copyWith( color: theme.colorScheme.primary, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 24), // Contenu principal avec ValueListenableBuilder Expanded( child: ValueListenableBuilder>( valueListenable: widget.operationRepository.operationBox.listenable(), builder: (context, operationBox, child) { debugPrint('🔄 ValueListenableBuilder - Nombre d\'opérations: ${operationBox.length}'); // Filtrer et trier les opérations final allOperations = operationBox.values.toList(); allOperations.sort((a, b) => b.id.compareTo(a.id)); final operations = allOperations.take(10).toList(); // Limiter à 10 opérations récentes debugPrint('📊 Opérations affichées: ${operations.length}'); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header avec bouton d'ajout Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Opérations récentes (${operations.length})', style: theme.textTheme.titleLarge?.copyWith( color: theme.colorScheme.primary, fontWeight: FontWeight.w600, ), ), ElevatedButton.icon( onPressed: _showCreateOperationDialog, icon: const Icon(Icons.add), label: const Text('Nouvelle opération'), style: ElevatedButton.styleFrom( backgroundColor: theme.colorScheme.primary, foregroundColor: Colors.white, ), ), ], ), const SizedBox(height: 16), // Tableau des opérations Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: operations.isEmpty ? Padding( padding: const EdgeInsets.all(32.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.calendar_today_outlined, size: 64, color: theme.colorScheme.primary.withValues(alpha: 0.5), ), const SizedBox(height: 16), Text( "Aucune opération créée", style: theme.textTheme.titleLarge?.copyWith( color: theme.colorScheme.primary, ), ), const SizedBox(height: 8), Text( "Cliquez sur 'Nouvelle opération' pour commencer", style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurface.withValues(alpha: 0.6), ), ), ], ), ) : _buildOperationsTable(operations), ), const SizedBox(height: 24), ], ); }, ), ), ], ), ); } }