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:
2026-01-26 16:57:08 +01:00
parent c24a3afe6a
commit 0687900564
3040 changed files with 77204 additions and 1578 deletions

View File

10
app/lib/presentation/widgets/btn_passages.dart Normal file → Executable file
View 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,
),

View File

0
app/lib/presentation/widgets/loading_spin_overlay.dart Normal file → Executable file
View File

View 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
View File

View 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
View File

View File

View File

View File

0
app/lib/presentation/widgets/result_dialog.dart Normal file → Executable file
View File

View 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,
}