import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:geosector_app/core/data/models/operation_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/presentation/widgets/custom_text_field.dart'; import 'package:geosector_app/presentation/widgets/result_dialog.dart'; import 'package:geosector_app/presentation/widgets/loading_spin_overlay.dart'; class OperationFormDialog extends StatefulWidget { final OperationModel? operation; final String title; final bool readOnly; final OperationRepository operationRepository; final UserRepository userRepository; final VoidCallback? onSuccess; const OperationFormDialog({ super.key, this.operation, required this.title, this.readOnly = false, required this.operationRepository, required this.userRepository, this.onSuccess, }); @override State createState() => _OperationFormDialogState(); } class _OperationFormDialogState extends State { final _formKey = GlobalKey(); bool _isSubmitting = false; // Controllers late final TextEditingController _nameController; late final TextEditingController _dateDebutController; late final TextEditingController _dateFinController; // Form values DateTime? _dateDebut; DateTime? _dateFin; @override void initState() { super.initState(); // Initialize controllers with operation data if available final operation = widget.operation; _nameController = TextEditingController(text: operation?.name ?? ''); _dateDebut = operation?.dateDebut; _dateFin = operation?.dateFin; _dateDebutController = TextEditingController( text: _dateDebut != null ? DateFormat('dd/MM/yyyy').format(_dateDebut!) : '', ); _dateFinController = TextEditingController( text: _dateFin != null ? DateFormat('dd/MM/yyyy').format(_dateFin!) : '', ); } @override void dispose() { _nameController.dispose(); _dateDebutController.dispose(); _dateFinController.dispose(); super.dispose(); } // Méthode pour sélectionner une date void _selectDate(BuildContext context, bool isDateDebut) { try { final DateTime initialDate; final DateTime firstDate; final DateTime lastDate; if (isDateDebut) { // Pour la date de début initialDate = _dateDebut ?? DateTime.now(); firstDate = DateTime(DateTime.now().year - 2); lastDate = _dateFin ?? DateTime(DateTime.now().year + 5); } else { // Pour la date de fin initialDate = _dateFin ?? (_dateDebut ?? DateTime.now()); firstDate = _dateDebut ?? DateTime(DateTime.now().year - 2); lastDate = DateTime(DateTime.now().year + 5); } showDatePicker( context: context, initialDate: initialDate, firstDate: firstDate, lastDate: lastDate, ).then((DateTime? picked) { if (picked != null) { setState(() { if (isDateDebut) { _dateDebut = picked; _dateDebutController.text = DateFormat('dd/MM/yyyy').format(picked); // Si la date de fin est antérieure à la nouvelle date de début, la réinitialiser if (_dateFin != null && _dateFin!.isBefore(picked)) { _dateFin = null; _dateFinController.clear(); } } else { _dateFin = picked; _dateFinController.text = DateFormat('dd/MM/yyyy').format(picked); } }); } }); } catch (e) { debugPrint('Exception lors de l\'affichage du sélecteur de date: $e'); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Impossible d\'afficher le sélecteur de date'), backgroundColor: Colors.red, ), ); } } void _handleSubmit() async { debugPrint('=== _handleSubmit APPELÉ ==='); if (_isSubmitting) { debugPrint('=== ARRÊT: En cours de soumission ==='); return; } // Valider le formulaire uniquement au submit if (!_formKey.currentState!.validate()) { debugPrint('=== ARRÊT: Formulaire invalide ==='); return; } debugPrint('=== DÉBUT SOUMISSION ==='); setState(() { _isSubmitting = true; }); // Afficher l'overlay de chargement final overlay = LoadingSpinOverlayUtils.show( context: context, message: 'Enregistrement en cours...', ); try { // Récupérer l'utilisateur actuel pour le fkEntite final currentUser = widget.userRepository.getCurrentUser(); final userFkEntite = currentUser?.fkEntite ?? 0; final operationData = widget.operation?.copyWith( name: _nameController.text.trim(), dateDebut: _dateDebut!, dateFin: _dateFin!, lastSyncedAt: DateTime.now(), ) ?? OperationModel( id: 0, name: _nameController.text.trim(), dateDebut: _dateDebut!, dateFin: _dateFin!, lastSyncedAt: DateTime.now(), fkEntite: userFkEntite, // ← Utiliser le fkEntite de l'utilisateur isActive: false, isSynced: false, ); debugPrint('=== OPERATION DATA ==='); debugPrint('operation.id: ${operationData.id}'); debugPrint('operation.fkEntite: ${operationData.fkEntite}'); debugPrint('user.fkEntite: $userFkEntite'); debugPrint('=== APPEL REPOSITORY ==='); // Appel direct du repository - la dialog gère tout final success = await widget.operationRepository.saveOperationFromModel(operationData); if (success && mounted) { debugPrint('=== SUCCÈS ==='); // Masquer le loading LoadingSpinOverlayUtils.hideSpecific(overlay); // Afficher le résultat de succès await ResultDialog.show( context: context, success: true, message: widget.operation == null ? "Nouvelle opération créée avec succès" : "Opération modifiée avec succès", ); // Auto-fermeture de la dialog if (mounted) { debugPrint('=== FERMETURE DIALOG ==='); try { Navigator.of(context).pop(); widget.onSuccess?.call(); } catch (e) { debugPrint('=== ERREUR Navigator.pop(): $e ==='); } } } else if (mounted) { debugPrint('=== ÉCHEC ==='); // Masquer le loading LoadingSpinOverlayUtils.hideSpecific(overlay); // Afficher l'erreur await ResultDialog.show( context: context, success: false, message: widget.operation == null ? "Échec de la création de l'opération" : "Échec de la mise à jour de l'opération", ); } } catch (e) { debugPrint('=== ERREUR dans _handleSubmit: $e ==='); // Masquer le loading LoadingSpinOverlayUtils.hideSpecific(overlay); // Afficher l'erreur if (mounted) { await ResultDialog.show( context: context, success: false, message: ApiException.fromError(e).message, ); } } finally { // Réinitialiser l'état de soumission seulement si le widget est encore monté if (mounted) { setState(() { _isSubmitting = false; }); } } } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Container( width: MediaQuery.of(context).size.width * 0.4, constraints: const BoxConstraints( maxWidth: 500, maxHeight: 700, ), padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Header Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Row( children: [ Icon( widget.operation == null ? Icons.add_circle : Icons.edit, color: theme.colorScheme.primary, ), const SizedBox(width: 8), Flexible( child: Text( widget.title, style: theme.textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.primary, ), overflow: TextOverflow.ellipsis, ), ), ], ), ), IconButton( icon: const Icon(Icons.close), onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(), ), ], ), const Divider(), // Contenu du formulaire Expanded( child: SingleChildScrollView( child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Nom de l'opération CustomTextField( controller: _nameController, label: "Nom de l'opération", readOnly: widget.readOnly, prefixIcon: Icons.event, isRequired: true, maxLength: 100, validator: (value) { if (value == null || value.trim().isEmpty) { return "Veuillez entrer le nom de l'opération"; } if (value.trim().length < 5) { return "Le nom doit contenir au moins 5 caractères"; } if (value.trim().length > 100) { return "Le nom ne peut pas dépasser 100 caractères"; } return null; }, hintText: "Ex: Calendriers 2024, Opération Noël...", ), const SizedBox(height: 24), // Section des dates Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( border: Border.all(color: theme.colorScheme.outline.withOpacity(0.5)), borderRadius: BorderRadius.circular(8), color: theme.colorScheme.surface.withOpacity(0.3), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.date_range, color: theme.colorScheme.primary, size: 20, ), const SizedBox(width: 8), Text( "Période de l'opération", style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, color: theme.colorScheme.primary, ), ), ], ), const SizedBox(height: 16), // Date de début CustomTextField( controller: _dateDebutController, label: "Date de début", readOnly: true, isRequired: true, onTap: widget.readOnly ? null : () => _selectDate(context, true), suffixIcon: Icon( Icons.calendar_today, color: theme.colorScheme.primary, ), validator: (value) { if (_dateDebut == null) { return "Veuillez sélectionner la date de début"; } return null; }, hintText: "Cliquez pour sélectionner la date", ), const SizedBox(height: 16), // Date de fin CustomTextField( controller: _dateFinController, label: "Date de fin", readOnly: true, isRequired: true, onTap: widget.readOnly ? null : () => _selectDate(context, false), suffixIcon: Icon( Icons.calendar_today, color: theme.colorScheme.primary, ), validator: (value) { if (_dateFin == null) { return "Veuillez sélectionner la date de fin"; } if (_dateDebut != null && _dateFin!.isBefore(_dateDebut!)) { return "La date de fin doit être postérieure à la date de début"; } if (_dateDebut != null && _dateFin!.isAtSameMomentAs(_dateDebut!)) { return "La date de fin doit être différente de la date de début"; } return null; }, hintText: "Cliquez pour sélectionner la date", ), // Indicateur de durée if (_dateDebut != null && _dateFin != null) ...[ const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: theme.colorScheme.primaryContainer, borderRadius: BorderRadius.circular(6), ), child: Row( children: [ Icon( Icons.info_outline, size: 16, color: theme.colorScheme.onPrimaryContainer, ), const SizedBox(width: 8), Text( "Durée: ${_dateFin!.difference(_dateDebut!).inDays + 1} jour(s)", style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onPrimaryContainer, fontWeight: FontWeight.w500, ), ), ], ), ), ], ], ), ), const SizedBox(height: 16), // Informations supplémentaires pour les nouvelles opérations if (widget.operation == null) ...[ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: theme.colorScheme.secondaryContainer.withOpacity(0.3), borderRadius: BorderRadius.circular(8), border: Border.all( color: theme.colorScheme.outline.withOpacity(0.3), ), ), child: Row( children: [ const Icon( Icons.lightbulb_outline, color: Colors.black87, size: 20, ), const SizedBox(width: 12), Expanded( child: Text( "La nouvelle opération sera activée automatiquement et remplacera l'opération active actuelle.", style: theme.textTheme.bodySmall?.copyWith( color: Colors.black87, ), ), ), ], ), ), ], ], ), ), ), ), const SizedBox(height: 24), // Footer avec boutons Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(), child: const Text('Annuler'), ), const SizedBox(width: 16), if (!widget.readOnly) ElevatedButton.icon( onPressed: _isSubmitting ? null : _handleSubmit, icon: _isSubmitting ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : Icon(widget.operation == null ? Icons.add : Icons.save), label: Text(_isSubmitting ? 'Enregistrement...' : (widget.operation == null ? 'Créer' : 'Enregistrer')), style: ElevatedButton.styleFrom( backgroundColor: theme.colorScheme.primary, foregroundColor: Colors.white, ), ), ], ), ], ), ), ); } }