fix: Récupérer l'opération active depuis la table operations
- Corrige l'erreur SQL 'Unknown column fk_operation in users' - L'opération active est récupérée depuis operations.chk_active = 1 - Jointure avec users pour filtrer par entité de l'admin créateur - Query: SELECT o.id FROM operations o INNER JOIN users u ON u.fk_entite = o.fk_entite WHERE u.id = ? AND o.chk_active = 1
This commit is contained in:
0
app/lib/presentation/widgets/badged_navigation_destination.dart
Normal file → Executable file
0
app/lib/presentation/widgets/badged_navigation_destination.dart
Normal file → Executable file
10
app/lib/presentation/widgets/btn_passages.dart
Normal file → Executable file
10
app/lib/presentation/widgets/btn_passages.dart
Normal file → Executable file
@@ -193,7 +193,8 @@ class BtnPassages extends StatelessWidget {
|
||||
Text(
|
||||
total > 1 ? 'passages' : 'passage',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
@@ -297,7 +298,8 @@ class BtnPassages extends StatelessWidget {
|
||||
child: Text(
|
||||
titre,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
@@ -371,9 +373,9 @@ class BtnPassages extends StatelessWidget {
|
||||
Text(
|
||||
'Nouveau',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[700],
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
0
app/lib/presentation/widgets/grouped_passages_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/grouped_passages_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/loading_spin_overlay.dart
Normal file → Executable file
0
app/lib/presentation/widgets/loading_spin_overlay.dart
Normal file → Executable file
48
app/lib/presentation/widgets/members_board_passages.dart
Normal file → Executable file
48
app/lib/presentation/widgets/members_board_passages.dart
Normal file → Executable file
@@ -402,14 +402,14 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
color: Colors.green.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[1]!['couleur2'] as int).withOpacity(0.2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.task_alt,
|
||||
size: 16,
|
||||
color: Colors.green,
|
||||
color: Color(AppKeys.typesPassages[1]!['couleur2'] as int),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_buildHeaderText('Effectués', 2, headerStyle),
|
||||
@@ -436,14 +436,14 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
color: Colors.orange.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[2]!['couleur2'] as int).withOpacity(0.2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.refresh,
|
||||
size: 16,
|
||||
color: Colors.orange,
|
||||
color: Color(AppKeys.typesPassages[2]!['couleur2'] as int),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_buildHeaderText('À finaliser', 4, headerStyle),
|
||||
@@ -460,14 +460,14 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
color: Colors.red.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[3]!['couleur2'] as int).withOpacity(0.2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.block,
|
||||
size: 16,
|
||||
color: Colors.red,
|
||||
color: Color(AppKeys.typesPassages[3]!['couleur2'] as int),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_buildHeaderText('Refusés', 5, headerStyle),
|
||||
@@ -484,14 +484,14 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
color: Colors.lightBlue.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[4]!['couleur2'] as int).withOpacity(0.2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.volunteer_activism,
|
||||
size: 16,
|
||||
color: Colors.lightBlue,
|
||||
color: Color(AppKeys.typesPassages[4]!['couleur2'] as int),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_buildHeaderText('Dons', 6, headerStyle),
|
||||
@@ -509,14 +509,14 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
color: Colors.blue.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[5]!['couleur2'] as int).withOpacity(0.2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.layers,
|
||||
size: 16,
|
||||
color: Colors.blue,
|
||||
color: Color(AppKeys.typesPassages[5]!['couleur2'] as int),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_buildHeaderText('Lots', 7, headerStyle),
|
||||
@@ -533,14 +533,14 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[6]!['couleur2'] as int).withOpacity(0.2),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.home_outlined,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
color: Color(AppKeys.typesPassages[6]!['couleur2'] as int),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_buildHeaderText('Vides', showLotType ? 8 : 7, headerStyle),
|
||||
@@ -712,7 +712,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.green.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[1]!['couleur2'] as int),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -752,7 +752,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.orange.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[2]!['couleur2'] as int),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
aFinaliserCount.toString(),
|
||||
@@ -773,7 +773,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.red.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[3]!['couleur2'] as int),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
refuseCount.toString(),
|
||||
@@ -794,7 +794,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.lightBlue.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[4]!['couleur2'] as int),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
donCount.toString(),
|
||||
@@ -816,7 +816,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.blue.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[5]!['couleur2'] as int),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -846,7 +846,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Color(AppKeys.typesPassages[6]!['couleur2'] as int),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
videCount.toString(),
|
||||
@@ -1074,7 +1074,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
color: Color(AppKeys.typesPassages[1]!['couleur2'] as int).withOpacity(0.8),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -1109,7 +1109,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.orange.withOpacity(0.1),
|
||||
color: Color(AppKeys.typesPassages[2]!['couleur2'] as int).withOpacity(0.8),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
aFinaliserCount.toString(),
|
||||
@@ -1130,7 +1130,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.red.withOpacity(0.1),
|
||||
color: Color(AppKeys.typesPassages[3]!['couleur2'] as int).withOpacity(0.8),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
refuseCount.toString(),
|
||||
@@ -1150,7 +1150,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.lightBlue.withOpacity(0.1),
|
||||
color: Color(AppKeys.typesPassages[4]!['couleur2'] as int).withOpacity(0.8),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
donCount.toString(),
|
||||
@@ -1171,7 +1171,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
color: Color(AppKeys.typesPassages[5]!['couleur2'] as int).withOpacity(0.8),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -1201,7 +1201,7 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
color: Color(AppKeys.typesPassages[6]!['couleur2'] as int).withOpacity(0.8),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
videCount.toString(),
|
||||
|
||||
0
app/lib/presentation/widgets/offline_test_button.dart
Normal file → Executable file
0
app/lib/presentation/widgets/offline_test_button.dart
Normal file → Executable file
@@ -89,17 +89,13 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
|
||||
// Helpers de validation
|
||||
String? _validateNumero(String? value) {
|
||||
debugPrint('🔍 [VALIDATOR] _validateNumero appelé avec: "$value"');
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
debugPrint('❌ [VALIDATOR] Numéro vide -> retourne erreur');
|
||||
return 'Le numéro est obligatoire';
|
||||
}
|
||||
final numero = int.tryParse(value.trim());
|
||||
if (numero == null || numero <= 0) {
|
||||
debugPrint('❌ [VALIDATOR] Numéro invalide: $value -> retourne erreur');
|
||||
return 'Numéro invalide';
|
||||
}
|
||||
debugPrint('✅ [VALIDATOR] Numéro valide: $numero');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -166,30 +162,11 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
super.initState();
|
||||
|
||||
try {
|
||||
debugPrint('=== DEBUT PassageFormDialog.initState ===');
|
||||
|
||||
// Accéder à la settingsBox (déjà ouverte dans l'app)
|
||||
_settingsBox = Hive.box(AppKeys.settingsBoxName);
|
||||
|
||||
// Initialize controllers with passage data if available
|
||||
final passage = widget.passage;
|
||||
debugPrint('Passage reçu: ${passage != null}');
|
||||
|
||||
if (passage != null) {
|
||||
debugPrint('Passage ID: ${passage.id}');
|
||||
debugPrint('Passage fkType: ${passage.fkType}');
|
||||
debugPrint('Passage numero: ${passage.numero}');
|
||||
debugPrint('Passage rueBis: ${passage.rueBis}');
|
||||
debugPrint('Passage rue: ${passage.rue}');
|
||||
debugPrint('Passage ville: ${passage.ville}');
|
||||
debugPrint('Passage name: ${passage.name}');
|
||||
debugPrint('Passage email: ${passage.email}');
|
||||
debugPrint('Passage phone: ${passage.phone}');
|
||||
debugPrint('Passage montant: ${passage.montant}');
|
||||
debugPrint('Passage remarque: ${passage.remarque}');
|
||||
debugPrint('Passage fkHabitat: ${passage.fkHabitat}');
|
||||
debugPrint('Passage fkTypeReglement: ${passage.fkTypeReglement}');
|
||||
}
|
||||
|
||||
_selectedPassageType = passage?.fkType;
|
||||
_showForm = false; // Toujours commencer par la sélection de type
|
||||
@@ -199,8 +176,6 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
// Section Adresse : ouverte si nouveau passage, fermée si modification
|
||||
_isAddressSectionExpanded = passage == null;
|
||||
|
||||
debugPrint('Initialisation des controllers...');
|
||||
|
||||
// S'assurer que toutes les valeurs null deviennent des chaînes vides
|
||||
String numero = passage?.numero.toString() ?? '';
|
||||
String rueBis = passage?.rueBis.toString() ?? '';
|
||||
@@ -222,7 +197,6 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
|
||||
// Si nouveau passage, charger les valeurs mémorisées de la dernière adresse
|
||||
if (passage == null) {
|
||||
debugPrint('Nouveau passage: chargement des valeurs mémorisées...');
|
||||
numero = _settingsBox.get('lastPassageNumero', defaultValue: '') as String;
|
||||
rueBis = _settingsBox.get('lastPassageRueBis', defaultValue: '') as String;
|
||||
rue = _settingsBox.get('lastPassageRue', defaultValue: '') as String;
|
||||
@@ -231,8 +205,6 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
_fkHabitat = _settingsBox.get('lastPassageFkHabitat', defaultValue: 1) as int;
|
||||
appt = _settingsBox.get('lastPassageAppt', defaultValue: '') as String;
|
||||
niveau = _settingsBox.get('lastPassageNiveau', defaultValue: '') as String;
|
||||
|
||||
debugPrint('Valeurs chargées: numero="$numero", rue="$rue", ville="$ville"');
|
||||
}
|
||||
|
||||
// Initialiser la date de passage
|
||||
@@ -242,20 +214,6 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
final String timeFormatted =
|
||||
'${_passedAt.hour.toString().padLeft(2, '0')}:${_passedAt.minute.toString().padLeft(2, '0')}';
|
||||
|
||||
debugPrint('Valeurs pour controllers:');
|
||||
debugPrint(' numero: "$numero"');
|
||||
debugPrint(' rueBis: "$rueBis"');
|
||||
debugPrint(' rue: "$rue"');
|
||||
debugPrint(' ville: "$ville"');
|
||||
debugPrint(' name: "$name"');
|
||||
debugPrint(' email: "$email"');
|
||||
debugPrint(' phone: "$phone"');
|
||||
debugPrint(' montant: "$montant"');
|
||||
debugPrint(' remarque: "$remarque"');
|
||||
debugPrint(' passedAt: "$_passedAt"');
|
||||
debugPrint(' dateFormatted: "$dateFormatted"');
|
||||
debugPrint(' timeFormatted: "$timeFormatted"');
|
||||
|
||||
_numeroController = TextEditingController(text: numero);
|
||||
_rueBisController = TextEditingController(text: rueBis);
|
||||
_rueController = TextEditingController(text: rue);
|
||||
@@ -280,12 +238,8 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
debugPrint('=== FIN PassageFormDialog.initState ===');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('=== ERREUR PassageFormDialog.initState ===');
|
||||
debugPrint('Erreur: $e');
|
||||
debugPrint('StackTrace: $stackTrace');
|
||||
debugPrint('❌ Erreur initState PassageFormDialog: $e\n$stackTrace');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -334,20 +288,11 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
}
|
||||
|
||||
void _handleSubmit() async {
|
||||
debugPrint('🔵 [SUBMIT] Début _handleSubmit');
|
||||
|
||||
if (_isSubmitting) {
|
||||
debugPrint('⚠️ [SUBMIT] Déjà en cours de soumission, abandon');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('🔵 [SUBMIT] Vérification de l\'état du formulaire');
|
||||
debugPrint('🔵 [SUBMIT] _formKey: $_formKey');
|
||||
debugPrint('🔵 [SUBMIT] _formKey.currentState: ${_formKey.currentState}');
|
||||
if (_isSubmitting) return;
|
||||
|
||||
// Validation avec protection contre le null
|
||||
if (_formKey.currentState == null) {
|
||||
debugPrint('❌ [SUBMIT] ERREUR: _formKey.currentState est null !');
|
||||
debugPrint('❌ _formKey.currentState est null');
|
||||
if (mounted) {
|
||||
await ResultDialog.show(
|
||||
context: context,
|
||||
@@ -358,14 +303,9 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('🔵 [SUBMIT] Validation du formulaire...');
|
||||
final isValid = _formKey.currentState!.validate();
|
||||
debugPrint('🔵 [SUBMIT] Résultat validation: $isValid');
|
||||
|
||||
if (!isValid) {
|
||||
debugPrint('⚠️ [SUBMIT] Validation échouée, abandon');
|
||||
|
||||
// Afficher un dialog d'erreur clair à l'utilisateur
|
||||
if (mounted) {
|
||||
await ResultDialog.show(
|
||||
context: context,
|
||||
@@ -376,109 +316,73 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('✅ [SUBMIT] Validation OK, appel _savePassage()');
|
||||
await _savePassage();
|
||||
debugPrint('🔵 [SUBMIT] Fin _handleSubmit');
|
||||
}
|
||||
|
||||
Future<void> _savePassage() async {
|
||||
debugPrint('🟢 [SAVE] Début _savePassage');
|
||||
if (_isSubmitting) return;
|
||||
|
||||
if (_isSubmitting) {
|
||||
debugPrint('⚠️ [SAVE] Déjà en cours de soumission, abandon');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('🟢 [SAVE] Mise à jour état _isSubmitting = true');
|
||||
setState(() {
|
||||
_isSubmitting = true;
|
||||
});
|
||||
|
||||
// Afficher l'overlay de chargement
|
||||
debugPrint('🟢 [SAVE] Affichage overlay de chargement');
|
||||
final overlay = LoadingSpinOverlayUtils.show(
|
||||
context: context,
|
||||
message: 'Enregistrement en cours...',
|
||||
);
|
||||
|
||||
try {
|
||||
debugPrint('🟢 [SAVE] Récupération utilisateur actuel');
|
||||
final currentUser = widget.userRepository.getCurrentUser();
|
||||
debugPrint('🟢 [SAVE] currentUser: ${currentUser?.id} - ${currentUser?.name}');
|
||||
|
||||
if (currentUser == null) {
|
||||
debugPrint('❌ [SAVE] ERREUR: Utilisateur non connecté');
|
||||
throw Exception("Utilisateur non connecté");
|
||||
}
|
||||
|
||||
debugPrint('🟢 [SAVE] Récupération opération active');
|
||||
final currentOperation = widget.operationRepository.getCurrentOperation();
|
||||
debugPrint('🟢 [SAVE] currentOperation: ${currentOperation?.id} - ${currentOperation?.name}');
|
||||
|
||||
if (currentOperation == null && widget.passage == null) {
|
||||
debugPrint('❌ [SAVE] ERREUR: Aucune opération active trouvée');
|
||||
throw Exception("Aucune opération active trouvée");
|
||||
}
|
||||
|
||||
// Déterminer les valeurs de montant et type de règlement selon le type de passage
|
||||
debugPrint('🟢 [SAVE] Calcul des valeurs finales');
|
||||
debugPrint('🟢 [SAVE] _selectedPassageType: $_selectedPassageType');
|
||||
|
||||
final String finalMontant =
|
||||
(_selectedPassageType == 1 || _selectedPassageType == 5)
|
||||
? _montantController.text.trim().replaceAll(',', '.')
|
||||
: '0';
|
||||
debugPrint('🟢 [SAVE] finalMontant: $finalMontant');
|
||||
|
||||
// Déterminer le type de règlement final selon le type de passage
|
||||
final int finalTypeReglement;
|
||||
if (_selectedPassageType == 1 || _selectedPassageType == 5) {
|
||||
// Pour les types 1 et 5, utiliser la valeur sélectionnée (qui a été validée)
|
||||
finalTypeReglement = _fkTypeReglement;
|
||||
} else {
|
||||
// Pour tous les autres types, forcer "Non renseigné"
|
||||
finalTypeReglement = 4;
|
||||
}
|
||||
debugPrint('🟢 [SAVE] finalTypeReglement: $finalTypeReglement');
|
||||
|
||||
// Déterminer la valeur de nbPassages selon le type de passage
|
||||
final int finalNbPassages;
|
||||
if (widget.passage != null) {
|
||||
// Modification d'un passage existant
|
||||
if (_selectedPassageType == 2) {
|
||||
// Type 2 (À finaliser) : toujours incrémenter
|
||||
finalNbPassages = widget.passage!.nbPassages + 1;
|
||||
} else {
|
||||
// Autres types : mettre à 1 si actuellement 0, sinon conserver
|
||||
final currentNbPassages = widget.passage!.nbPassages;
|
||||
finalNbPassages = currentNbPassages == 0 ? 1 : currentNbPassages;
|
||||
}
|
||||
} else {
|
||||
// Nouveau passage : toujours 1
|
||||
finalNbPassages = 1;
|
||||
}
|
||||
debugPrint('🟢 [SAVE] finalNbPassages: $finalNbPassages');
|
||||
|
||||
// Récupérer les coordonnées GPS pour un nouveau passage
|
||||
String finalGpsLat = '0.0';
|
||||
String finalGpsLng = '0.0';
|
||||
if (widget.passage == null) {
|
||||
// Nouveau passage : tenter de récupérer la position GPS actuelle
|
||||
debugPrint('🟢 [SAVE] Récupération de la position GPS...');
|
||||
try {
|
||||
final position = await LocationService.getCurrentPosition();
|
||||
if (position != null) {
|
||||
finalGpsLat = position.latitude.toString();
|
||||
finalGpsLng = position.longitude.toString();
|
||||
debugPrint('🟢 [SAVE] GPS récupéré: lat=$finalGpsLat, lng=$finalGpsLng');
|
||||
} else {
|
||||
debugPrint('🟢 [SAVE] GPS non disponible, utilisation de 0.0 (l\'API utilisera le géocodage)');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ [SAVE] Erreur récupération GPS: $e - l\'API utilisera le géocodage');
|
||||
}
|
||||
} catch (_) {}
|
||||
} else {
|
||||
// Modification : conserver les coordonnées existantes
|
||||
finalGpsLat = widget.passage!.gpsLat;
|
||||
finalGpsLng = widget.passage!.gpsLng;
|
||||
}
|
||||
@@ -537,38 +441,25 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
isSynced: false,
|
||||
);
|
||||
|
||||
// Sauvegarder le passage d'abord
|
||||
debugPrint('🟢 [SAVE] Préparation sauvegarde passage');
|
||||
// Sauvegarder le passage
|
||||
PassageModel? savedPassage;
|
||||
if (widget.passage == null || widget.passage!.id == 0) {
|
||||
// Création d'un nouveau passage (passage null OU id=0)
|
||||
debugPrint('🟢 [SAVE] Création d\'un nouveau passage');
|
||||
savedPassage = await widget.passageRepository.createPassageWithReturn(passageData);
|
||||
debugPrint('🟢 [SAVE] Passage créé avec ID: ${savedPassage?.id}');
|
||||
|
||||
if (savedPassage == null) {
|
||||
debugPrint('❌ [SAVE] ERREUR: savedPassage est null après création');
|
||||
throw Exception("Échec de la création du passage");
|
||||
}
|
||||
} else {
|
||||
// Mise à jour d'un passage existant
|
||||
debugPrint('🟢 [SAVE] Mise à jour passage existant ID: ${widget.passage!.id}');
|
||||
await widget.passageRepository.updatePassage(passageData);
|
||||
debugPrint('🟢 [SAVE] Mise à jour réussie');
|
||||
savedPassage = passageData;
|
||||
}
|
||||
|
||||
// Garantir le type non-nullable après la vérification
|
||||
final confirmedPassage = savedPassage;
|
||||
debugPrint('✅ [SAVE] Passage sauvegardé avec succès ID: ${confirmedPassage.id}');
|
||||
|
||||
// Mémoriser l'adresse pour la prochaine création de passage
|
||||
debugPrint('🟢 [SAVE] Mémorisation adresse');
|
||||
await _saveLastPassageAddress();
|
||||
|
||||
// Propager la résidence aux autres passages de l'immeuble si nécessaire
|
||||
if (_fkHabitat == 2 && _residenceController.text.trim().isNotEmpty) {
|
||||
debugPrint('🟢 [SAVE] Propagation résidence à l\'immeuble');
|
||||
await _propagateResidenceToBuilding(confirmedPassage);
|
||||
}
|
||||
|
||||
@@ -605,16 +496,12 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
final paymentSuccess = await _attemptTapToPayWithPassage(confirmedPassage, montant);
|
||||
|
||||
if (paymentSuccess) {
|
||||
// Fermer le formulaire en cas de succès
|
||||
if (mounted) {
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
widget.onSuccess?.call();
|
||||
}
|
||||
} else {
|
||||
debugPrint('⚠️ Paiement Tap to Pay échoué - formulaire reste ouvert');
|
||||
// Ne pas fermer le formulaire en cas d'échec
|
||||
// L'utilisateur peut réessayer ou annuler
|
||||
}
|
||||
// Si échec, le formulaire reste ouvert pour réessayer
|
||||
},
|
||||
onQRCodeCompleted: () {
|
||||
// Pour QR Code: fermer le formulaire après l'affichage du QR
|
||||
@@ -667,17 +554,10 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
}
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
// Masquer le loading
|
||||
debugPrint('❌ [SAVE] ERREUR CAPTURÉE');
|
||||
debugPrint('❌ [SAVE] Type erreur: ${e.runtimeType}');
|
||||
debugPrint('❌ [SAVE] Message erreur: $e');
|
||||
debugPrint('❌ [SAVE] Stack trace:\n$stackTrace');
|
||||
|
||||
debugPrint('❌ Erreur sauvegarde passage: $e\n$stackTrace');
|
||||
LoadingSpinOverlayUtils.hideSpecific(overlay);
|
||||
|
||||
// Afficher l'erreur
|
||||
final errorMessage = ApiException.fromError(e).message;
|
||||
debugPrint('❌ [SAVE] Message d\'erreur formaté: $errorMessage');
|
||||
|
||||
if (mounted) {
|
||||
await ResultDialog.show(
|
||||
@@ -687,23 +567,17 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
debugPrint('🟢 [SAVE] Bloc finally - Nettoyage');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSubmitting = false;
|
||||
});
|
||||
debugPrint('🟢 [SAVE] _isSubmitting = false');
|
||||
}
|
||||
debugPrint('🟢 [SAVE] Fin _savePassage');
|
||||
}
|
||||
}
|
||||
|
||||
/// Mémoriser l'adresse du passage pour la prochaine création
|
||||
Future<void> _saveLastPassageAddress() async {
|
||||
try {
|
||||
debugPrint('🟡 [ADDRESS] Début mémorisation adresse');
|
||||
debugPrint('🟡 [ADDRESS] _settingsBox.isOpen: ${_settingsBox.isOpen}');
|
||||
|
||||
await _settingsBox.put('lastPassageNumero', _numeroController.text.trim());
|
||||
await _settingsBox.put('lastPassageRueBis', _rueBisController.text.trim());
|
||||
await _settingsBox.put('lastPassageRue', _rueController.text.trim());
|
||||
@@ -712,61 +586,34 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
await _settingsBox.put('lastPassageFkHabitat', _fkHabitat);
|
||||
await _settingsBox.put('lastPassageAppt', _apptController.text.trim());
|
||||
await _settingsBox.put('lastPassageNiveau', _niveauController.text.trim());
|
||||
|
||||
debugPrint('✅ [ADDRESS] Adresse mémorisée avec succès');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ [ADDRESS] Erreur lors de la mémorisation: $e');
|
||||
debugPrint('❌ [ADDRESS] Stack trace:\n$stackTrace');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur mémorisation adresse: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Propager la résidence aux autres passages de l'immeuble (fkType=2, même adresse, résidence vide)
|
||||
Future<void> _propagateResidenceToBuilding(PassageModel savedPassage) async {
|
||||
try {
|
||||
debugPrint('🟡 [PROPAGATE] Début propagation résidence');
|
||||
|
||||
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
||||
debugPrint('🟡 [PROPAGATE] passagesBox.isOpen: ${passagesBox.isOpen}');
|
||||
debugPrint('🟡 [PROPAGATE] passagesBox.length: ${passagesBox.length}');
|
||||
|
||||
final residence = _residenceController.text.trim();
|
||||
debugPrint('🟡 [PROPAGATE] résidence: "$residence"');
|
||||
|
||||
// Clé d'adresse du passage sauvegardé
|
||||
final addressKey = '${savedPassage.numero}|${savedPassage.rueBis}|${savedPassage.rue}|${savedPassage.ville}';
|
||||
debugPrint('🟡 [PROPAGATE] addressKey: "$addressKey"');
|
||||
|
||||
int updatedCount = 0;
|
||||
|
||||
// Parcourir tous les passages
|
||||
for (int i = 0; i < passagesBox.length; i++) {
|
||||
final passage = passagesBox.getAt(i);
|
||||
if (passage != null) {
|
||||
// Vérifier les critères
|
||||
final passageAddressKey = '${passage.numero}|${passage.rueBis}|${passage.rue}|${passage.ville}';
|
||||
|
||||
if (passage.id != savedPassage.id && // Pas le passage actuel
|
||||
passage.fkHabitat == 2 && // Appartement
|
||||
passageAddressKey == addressKey && // Même adresse
|
||||
passage.residence.trim().isEmpty) { // Résidence vide
|
||||
|
||||
debugPrint('🟡 [PROPAGATE] Mise à jour passage ID: ${passage.id}');
|
||||
// Mettre à jour la résidence dans Hive
|
||||
if (passage.id != savedPassage.id &&
|
||||
passage.fkHabitat == 2 &&
|
||||
passageAddressKey == addressKey &&
|
||||
passage.residence.trim().isEmpty) {
|
||||
final updatedPassage = passage.copyWith(residence: residence);
|
||||
await passagesBox.put(passage.key, updatedPassage);
|
||||
updatedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedCount > 0) {
|
||||
debugPrint('✅ [PROPAGATE] Résidence propagée à $updatedCount passage(s)');
|
||||
} else {
|
||||
debugPrint('✅ [PROPAGATE] Aucun passage à mettre à jour');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ [PROPAGATE] Erreur lors de la propagation: $e');
|
||||
debugPrint('❌ [PROPAGATE] Stack trace:\n$stackTrace');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur propagation résidence: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -780,24 +627,110 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
if (currentUser != null && currentUser.fkEntite != null) {
|
||||
final userAmicale = widget.amicaleRepository.getAmicaleById(currentUser.fkEntite!);
|
||||
if (userAmicale != null) {
|
||||
// Si chkLotActif = false (0), on ne doit pas afficher le type Lot (5)
|
||||
showLotType = userAmicale.chkLotActif;
|
||||
debugPrint('Amicale ${userAmicale.name}: chkLotActif = $showLotType');
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer les types de passages en fonction de chkLotActif
|
||||
final filteredTypes = Map<int, Map<String, dynamic>>.from(AppKeys.typesPassages);
|
||||
if (!showLotType) {
|
||||
filteredTypes.remove(5); // Retirer le type "Lot" si chkLotActif = 0
|
||||
debugPrint('Type Lot (5) masqué car chkLotActif = false');
|
||||
filteredTypes.remove(5);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Afficher les infos du passage si modification
|
||||
if (widget.passage != null) ...[
|
||||
// Adresse du passage
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.primary.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Adresse principale
|
||||
Text(
|
||||
'${widget.passage!.numero} ${widget.passage!.rueBis} ${widget.passage!.rue}'.trim().replaceAll(RegExp(r'\s+'), ' '),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.passage!.ville,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
// Infos appartement si fkHabitat == 2
|
||||
if (widget.passage!.fkHabitat == 2) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
if (widget.passage!.niveau.isNotEmpty) ...[
|
||||
Icon(Icons.stairs, size: 16, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 4),
|
||||
Text('Niveau ${widget.passage!.niveau}'),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
if (widget.passage!.appt.isNotEmpty) ...[
|
||||
Icon(Icons.door_front_door, size: 16, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 4),
|
||||
Text('Appt ${widget.passage!.appt}'),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
// Afficher le nom de l'habitant (pour maison et appartement)
|
||||
if (widget.passage!.name.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.person, size: 16, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(child: Text(widget.passage!.name)),
|
||||
],
|
||||
),
|
||||
],
|
||||
// Afficher la remarque si renseignée
|
||||
if (widget.passage!.remarque.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.note, size: 16, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.passage!.remarque,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
Text(
|
||||
'Type de passage',
|
||||
widget.passage != null
|
||||
? 'Choisir le nouveau type de ce passage'
|
||||
: 'Type de passage',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
@@ -810,7 +743,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio:
|
||||
MediaQuery.of(context).size.width < 600 ? 1.8 : 2.5,
|
||||
MediaQuery.of(context).size.width < 600 ? 1.4 : 2.5,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
@@ -821,7 +754,6 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
final typeData = filteredTypes[typeId];
|
||||
|
||||
if (typeData == null) {
|
||||
debugPrint('ERREUR: typeData null pour typeId: $typeId');
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
@@ -881,8 +813,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('ERREUR dans itemBuilder pour index $index: $e');
|
||||
} catch (_) {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
@@ -893,9 +824,6 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
|
||||
Widget _buildPassageForm() {
|
||||
try {
|
||||
debugPrint('=== DEBUT _buildPassageForm ===');
|
||||
|
||||
debugPrint('Building Form...');
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
@@ -1362,11 +1290,8 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
],
|
||||
),
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('=== ERREUR _buildPassageForm ===');
|
||||
debugPrint('Erreur: $e');
|
||||
debugPrint('StackTrace: $stackTrace');
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur _buildPassageForm: $e');
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
@@ -1526,17 +1451,10 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!_showForm) ...[
|
||||
() {
|
||||
debugPrint('Building passage type selection...');
|
||||
return _buildPassageTypeSelection();
|
||||
}(),
|
||||
] else ...[
|
||||
() {
|
||||
debugPrint('Building passage form...');
|
||||
return _buildPassageForm();
|
||||
}(),
|
||||
],
|
||||
if (!_showForm)
|
||||
_buildPassageTypeSelection()
|
||||
else
|
||||
_buildPassageForm(),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -1690,10 +1608,9 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
);
|
||||
|
||||
// Envoyer la mise à jour à l'API (sera fait de manière asynchrone)
|
||||
widget.passageRepository.updatePassage(updatedPassage).then((_) {
|
||||
debugPrint('✅ Passage mis à jour avec stripe_payment_id: $paymentIntentId');
|
||||
}).catchError((error) {
|
||||
debugPrint('❌ Erreur mise à jour passage: $error');
|
||||
widget.passageRepository.updatePassage(updatedPassage).catchError((error) {
|
||||
debugPrint('❌ Erreur mise à jour passage stripe: $error');
|
||||
return false;
|
||||
});
|
||||
|
||||
setState(() {
|
||||
@@ -1720,7 +1637,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur Tap to Pay: $e');
|
||||
debugPrint('❌ Erreur Tap to Pay: $e');
|
||||
if (mounted) {
|
||||
await ResultDialog.show(
|
||||
context: context,
|
||||
@@ -1735,10 +1652,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
try {
|
||||
debugPrint('=== DEBUT PassageFormDialog.build ===');
|
||||
|
||||
final isMobile = _isMobile(context);
|
||||
debugPrint('Platform mobile détectée: $isMobile');
|
||||
|
||||
if (isMobile) {
|
||||
// Mode plein écran pour mobile
|
||||
@@ -1786,12 +1700,8 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('=== ERREUR PassageFormDialog.build ===');
|
||||
debugPrint('Erreur: $e');
|
||||
debugPrint('StackTrace: $stackTrace');
|
||||
|
||||
// Retourner un widget d'erreur simple
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur PassageFormDialog.build: $e');
|
||||
return Dialog(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -1980,9 +1890,7 @@ class _TapToPayFlowDialogState extends State<_TapToPayFlowDialog> {
|
||||
|
||||
// Annuler le PaymentIntent si créé pour permettre une nouvelle tentative
|
||||
if (shouldCancelPayment && _paymentIntentId != null) {
|
||||
StripeTapToPayService.instance.cancelPayment(_paymentIntentId!).catchError((cancelError) {
|
||||
debugPrint('⚠️ Erreur annulation PaymentIntent: $cancelError');
|
||||
});
|
||||
StripeTapToPayService.instance.cancelPayment(_paymentIntentId!).catchError((_) {});
|
||||
}
|
||||
|
||||
setState(() {
|
||||
|
||||
0
app/lib/presentation/widgets/passage_map_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/passage_map_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/payment_method_selection_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/payment_method_selection_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/pending_requests_counter.dart
Normal file → Executable file
0
app/lib/presentation/widgets/pending_requests_counter.dart
Normal file → Executable file
0
app/lib/presentation/widgets/qr_code_payment_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/qr_code_payment_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/result_dialog.dart
Normal file → Executable file
0
app/lib/presentation/widgets/result_dialog.dart
Normal file → Executable file
@@ -1,231 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/core/services/theme_service.dart';
|
||||
|
||||
/// Widget pour basculer entre les thèmes clair/sombre/automatique
|
||||
class ThemeSwitcher extends StatelessWidget {
|
||||
/// Style d'affichage du sélecteur
|
||||
final ThemeSwitcherStyle style;
|
||||
|
||||
/// Afficher le texte descriptif
|
||||
final bool showLabel;
|
||||
|
||||
/// Callback optionnel appelé après changement de thème
|
||||
final VoidCallback? onThemeChanged;
|
||||
|
||||
const ThemeSwitcher({
|
||||
super.key,
|
||||
this.style = ThemeSwitcherStyle.iconButton,
|
||||
this.showLabel = false,
|
||||
this.onThemeChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: ThemeService.instance,
|
||||
builder: (context, child) {
|
||||
switch (style) {
|
||||
case ThemeSwitcherStyle.iconButton:
|
||||
return _buildIconButton(context);
|
||||
case ThemeSwitcherStyle.dropdown:
|
||||
return _buildDropdown(context);
|
||||
case ThemeSwitcherStyle.segmentedButton:
|
||||
return _buildSegmentedButton(context);
|
||||
case ThemeSwitcherStyle.toggleButtons:
|
||||
return _buildToggleButtons(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Bouton icône simple (bascule entre clair/sombre)
|
||||
Widget _buildIconButton(BuildContext context) {
|
||||
final themeService = ThemeService.instance;
|
||||
|
||||
return IconButton(
|
||||
icon: Icon(themeService.themeModeIcon),
|
||||
tooltip: 'Changer le thème (${themeService.themeModeDescription})',
|
||||
onPressed: () async {
|
||||
await themeService.toggleTheme();
|
||||
onThemeChanged?.call();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Dropdown avec toutes les options
|
||||
Widget _buildDropdown(BuildContext context) {
|
||||
final themeService = ThemeService.instance;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return DropdownButton<ThemeMode>(
|
||||
value: themeService.themeMode,
|
||||
icon: Icon(Icons.arrow_drop_down, color: theme.colorScheme.onSurface),
|
||||
underline: Container(),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: ThemeMode.system,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.brightness_auto, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Automatique'),
|
||||
if (showLabel) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'(${themeService.isSystemDark ? 'sombre' : 'clair'})',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const DropdownMenuItem(
|
||||
value: ThemeMode.light,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.light_mode, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text('Clair'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const DropdownMenuItem(
|
||||
value: ThemeMode.dark,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.dark_mode, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text('Sombre'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (ThemeMode? mode) async {
|
||||
if (mode != null) {
|
||||
await themeService.setThemeMode(mode);
|
||||
onThemeChanged?.call();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Boutons segmentés (Material 3)
|
||||
Widget _buildSegmentedButton(BuildContext context) {
|
||||
final themeService = ThemeService.instance;
|
||||
|
||||
return SegmentedButton<ThemeMode>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: ThemeMode.light,
|
||||
icon: Icon(Icons.light_mode, size: 16),
|
||||
label: Text('Clair'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ThemeMode.system,
|
||||
icon: Icon(Icons.brightness_auto, size: 16),
|
||||
label: Text('Auto'),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ThemeMode.dark,
|
||||
icon: Icon(Icons.dark_mode, size: 16),
|
||||
label: Text('Sombre'),
|
||||
),
|
||||
],
|
||||
selected: {themeService.themeMode},
|
||||
onSelectionChanged: (Set<ThemeMode> selection) async {
|
||||
if (selection.isNotEmpty) {
|
||||
await themeService.setThemeMode(selection.first);
|
||||
onThemeChanged?.call();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Boutons à bascule
|
||||
Widget _buildToggleButtons(BuildContext context) {
|
||||
final themeService = ThemeService.instance;
|
||||
|
||||
return ToggleButtons(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
constraints: const BoxConstraints(minHeight: 40, minWidth: 60),
|
||||
isSelected: [
|
||||
themeService.themeMode == ThemeMode.light,
|
||||
themeService.themeMode == ThemeMode.system,
|
||||
themeService.themeMode == ThemeMode.dark,
|
||||
],
|
||||
onPressed: (int index) async {
|
||||
final modes = [ThemeMode.light, ThemeMode.system, ThemeMode.dark];
|
||||
await themeService.setThemeMode(modes[index]);
|
||||
onThemeChanged?.call();
|
||||
},
|
||||
children: const [
|
||||
Icon(Icons.light_mode, size: 20),
|
||||
Icon(Icons.brightness_auto, size: 20),
|
||||
Icon(Icons.dark_mode, size: 20),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget d'information sur le thème actuel
|
||||
class ThemeInfo extends StatelessWidget {
|
||||
const ThemeInfo({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: ThemeService.instance,
|
||||
builder: (context, child) {
|
||||
final themeService = ThemeService.instance;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outline.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
themeService.themeModeIcon,
|
||||
size: 16,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
themeService.themeModeDescription,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Styles d'affichage pour le ThemeSwitcher
|
||||
enum ThemeSwitcherStyle {
|
||||
/// Bouton icône simple qui bascule entre clair/sombre
|
||||
iconButton,
|
||||
|
||||
/// Menu déroulant avec toutes les options
|
||||
dropdown,
|
||||
|
||||
/// Boutons segmentés (Material 3)
|
||||
segmentedButton,
|
||||
|
||||
/// Boutons à bascule
|
||||
toggleButtons,
|
||||
}
|
||||
Reference in New Issue
Block a user