Livraison d ela gestion des opérations v0.4.0

This commit is contained in:
d6soft
2025-06-24 13:01:43 +02:00
parent 25c9d5874c
commit 416d648a14
813 changed files with 234012 additions and 73933 deletions

View File

@@ -0,0 +1,491 @@
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';
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<OperationFormDialog> createState() => _OperationFormDialogState();
}
class _OperationFormDialogState extends State<OperationFormDialog> {
final _formKey = GlobalKey<FormState>();
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;
});
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 - AUTO-FERMETURE ===');
debugPrint('=== context.mounted: ${context.mounted} ===');
// Délai pour laisser le temps à Hive de se synchroniser
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) {
debugPrint('=== FERMETURE DIFFÉRÉE ===');
// Auto-fermeture de la dialog
try {
debugPrint('=== AVANT Navigator.pop() ===');
Navigator.of(context).pop();
debugPrint('=== APRÈS Navigator.pop() ===');
} catch (e) {
debugPrint('=== ERREUR Navigator.pop(): $e ===');
}
// Notifier la page parente pour setState()
debugPrint('=== AVANT onSuccess?.call() ===');
widget.onSuccess?.call();
debugPrint('=== APRÈS onSuccess?.call() ===');
// Message de succès
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
debugPrint('=== AFFICHAGE MESSAGE SUCCÈS ===');
ApiException.showSuccess(context, widget.operation == null ? "Nouvelle opération créée avec succès" : "Opération modifiée avec succès");
}
});
}
});
} else if (mounted) {
debugPrint('=== ÉCHEC - AFFICHAGE ERREUR ===');
ApiException.showError(context, Exception(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 ===');
if (mounted) {
ApiException.showError(context, e);
}
} 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,
),
),
],
),
],
),
),
);
}
}