feat: Version 3.6.2 - Correctifs tâches #17-20
- #17: Amélioration gestion des secteurs et statistiques - #18: Optimisation services API et logs - #19: Corrections Flutter widgets et repositories - #20: Fix création passage - détection automatique ope_users.id vs users.id Suppression dossier web/ (migration vers app Flutter) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -305,6 +305,11 @@ class NavigationHelper {
|
||||
selectedIcon: Icon(Icons.calendar_today),
|
||||
label: 'Opérations',
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.analytics_outlined),
|
||||
selectedIcon: Icon(Icons.analytics),
|
||||
label: 'Connexions',
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -341,6 +346,9 @@ class NavigationHelper {
|
||||
case 5:
|
||||
context.go('/admin/operations');
|
||||
break;
|
||||
case 6:
|
||||
context.go('/admin/connexions');
|
||||
break;
|
||||
default:
|
||||
context.go('/admin');
|
||||
}
|
||||
@@ -380,6 +388,7 @@ class NavigationHelper {
|
||||
if (cleanRoute.contains('/admin/messages')) return 3;
|
||||
if (cleanRoute.contains('/admin/amicale')) return 4;
|
||||
if (cleanRoute.contains('/admin/operations')) return 5;
|
||||
if (cleanRoute.contains('/admin/connexions')) return 6;
|
||||
return 0; // Dashboard par défaut
|
||||
} else {
|
||||
if (cleanRoute.contains('/user/history')) return 1;
|
||||
@@ -400,6 +409,7 @@ class NavigationHelper {
|
||||
case 3: return 'messages';
|
||||
case 4: return 'amicale';
|
||||
case 5: return 'operations';
|
||||
case 6: return 'connexions';
|
||||
default: return 'dashboard';
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -124,66 +124,71 @@ class _MembersBoardPassagesState extends State<MembersBoardPassages> {
|
||||
),
|
||||
),
|
||||
|
||||
// Corps avec le tableau
|
||||
// Corps avec le tableau - écoute membres ET passages pour rafraîchissement auto
|
||||
ValueListenableBuilder<Box<MembreModel>>(
|
||||
valueListenable: Hive.box<MembreModel>(AppKeys.membresBoxName).listenable(),
|
||||
builder: (context, membresBox, child) {
|
||||
final membres = membresBox.values.toList();
|
||||
return ValueListenableBuilder<Box<PassageModel>>(
|
||||
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
|
||||
builder: (context, passagesBox, child) {
|
||||
final membres = membresBox.values.toList();
|
||||
|
||||
// Récupérer l'opération courante
|
||||
final currentOperation = _operationRepository.getCurrentOperation();
|
||||
if (currentOperation == null) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(AppTheme.spacingL),
|
||||
child: Text('Aucune opération en cours'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Trier les membres selon la colonne sélectionnée
|
||||
_sortMembers(membres, currentOperation.id);
|
||||
|
||||
// Construire les lignes : TOTAL en première position + détails membres
|
||||
final allRows = [
|
||||
_buildTotalRow(membres, currentOperation.id, theme),
|
||||
..._buildRows(membres, currentOperation.id, theme),
|
||||
];
|
||||
|
||||
// Afficher le tableau complet sans scroll interne
|
||||
return SizedBox(
|
||||
width: double.infinity, // Prendre toute la largeur disponible
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
dataTableTheme: DataTableThemeData(
|
||||
headingRowColor: WidgetStateProperty.resolveWith<Color?>(
|
||||
(Set<WidgetState> states) {
|
||||
return theme.colorScheme.primary.withOpacity(0.08);
|
||||
},
|
||||
// Récupérer l'opération courante
|
||||
final currentOperation = _operationRepository.getCurrentOperation();
|
||||
if (currentOperation == null) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(AppTheme.spacingL),
|
||||
child: Text('Aucune opération en cours'),
|
||||
),
|
||||
dataRowColor: WidgetStateProperty.resolveWith<Color?>(
|
||||
(Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return theme.colorScheme.primary.withOpacity(0.08);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Trier les membres selon la colonne sélectionnée
|
||||
_sortMembers(membres, currentOperation.id);
|
||||
|
||||
// Construire les lignes : TOTAL en première position + détails membres
|
||||
final allRows = [
|
||||
_buildTotalRow(membres, currentOperation.id, theme),
|
||||
..._buildRows(membres, currentOperation.id, theme),
|
||||
];
|
||||
|
||||
// Afficher le tableau complet sans scroll interne
|
||||
return SizedBox(
|
||||
width: double.infinity, // Prendre toute la largeur disponible
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
dataTableTheme: DataTableThemeData(
|
||||
headingRowColor: WidgetStateProperty.resolveWith<Color?>(
|
||||
(Set<WidgetState> states) {
|
||||
return theme.colorScheme.primary.withOpacity(0.08);
|
||||
},
|
||||
),
|
||||
dataRowColor: WidgetStateProperty.resolveWith<Color?>(
|
||||
(Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return theme.colorScheme.primary.withOpacity(0.08);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
child: DataTable(
|
||||
columnSpacing: 4, // Espacement minimal entre colonnes
|
||||
horizontalMargin: 4, // Marges horizontales minimales
|
||||
headingRowHeight: 42, // Hauteur de l'en-tête optimisée
|
||||
dataRowMinHeight: 42,
|
||||
dataRowMaxHeight: 42,
|
||||
// Utiliser les flèches natives de DataTable
|
||||
sortColumnIndex: _sortColumnIndex,
|
||||
sortAscending: _sortAscending,
|
||||
columns: _buildColumns(theme),
|
||||
rows: allRows,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: DataTable(
|
||||
columnSpacing: 4, // Espacement minimal entre colonnes
|
||||
horizontalMargin: 4, // Marges horizontales minimales
|
||||
headingRowHeight: 42, // Hauteur de l'en-tête optimisée
|
||||
dataRowMinHeight: 42,
|
||||
dataRowMaxHeight: 42,
|
||||
// Utiliser les flèches natives de DataTable
|
||||
sortColumnIndex: _sortColumnIndex,
|
||||
sortAscending: _sortAscending,
|
||||
columns: _buildColumns(theme),
|
||||
rows: allRows,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -79,6 +79,16 @@ class MembreRowWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// Tournée (sectName) - masqué en mobile
|
||||
if (!isMobile)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
membre.sectName ?? '',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
|
||||
// Email - masqué en mobile
|
||||
if (!isMobile)
|
||||
Expanded(
|
||||
|
||||
@@ -113,6 +113,19 @@ class MembreTableWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// Tournée (sectName) - masqué en mobile
|
||||
if (!isMobile)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
'Tournée',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Email - masqué en mobile
|
||||
if (!isMobile)
|
||||
Expanded(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/core/theme/app_theme.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/foundation.dart' show kIsWeb, debugPrint;
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||
import 'package:geosector_app/core/repositories/passage_repository.dart';
|
||||
@@ -15,6 +15,7 @@ import 'package:geosector_app/core/services/stripe_connect_service.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart';
|
||||
import 'package:geosector_app/core/utils/api_exception.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/core/services/location_service.dart';
|
||||
import 'package:geosector_app/presentation/widgets/custom_text_field.dart';
|
||||
import 'package:geosector_app/presentation/widgets/form_section.dart';
|
||||
import 'package:geosector_app/presentation/widgets/result_dialog.dart';
|
||||
@@ -88,13 +89,17 @@ 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;
|
||||
}
|
||||
|
||||
@@ -329,48 +334,102 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
}
|
||||
|
||||
void _handleSubmit() async {
|
||||
if (_isSubmitting) return;
|
||||
debugPrint('🔵 [SUBMIT] Début _handleSubmit');
|
||||
|
||||
// ✅ Validation intégrée avec focus automatique sur erreur
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
// Le focus est automatiquement mis sur le premier champ en erreur
|
||||
// Les bordures rouges et messages d'erreur sont affichés automatiquement
|
||||
if (_isSubmitting) {
|
||||
debugPrint('⚠️ [SUBMIT] Déjà en cours de soumission, abandon');
|
||||
return;
|
||||
}
|
||||
|
||||
// Toujours sauvegarder le passage en premier
|
||||
debugPrint('🔵 [SUBMIT] Vérification de l\'état du formulaire');
|
||||
debugPrint('🔵 [SUBMIT] _formKey: $_formKey');
|
||||
debugPrint('🔵 [SUBMIT] _formKey.currentState: ${_formKey.currentState}');
|
||||
|
||||
// Validation avec protection contre le null
|
||||
if (_formKey.currentState == null) {
|
||||
debugPrint('❌ [SUBMIT] ERREUR: _formKey.currentState est null !');
|
||||
if (mounted) {
|
||||
await ResultDialog.show(
|
||||
context: context,
|
||||
success: false,
|
||||
message: "Erreur d'initialisation du formulaire",
|
||||
);
|
||||
}
|
||||
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,
|
||||
success: false,
|
||||
message: 'Veuillez vérifier tous les champs marqués comme obligatoires',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('✅ [SUBMIT] Validation OK, appel _savePassage()');
|
||||
await _savePassage();
|
||||
debugPrint('🔵 [SUBMIT] Fin _handleSubmit');
|
||||
}
|
||||
|
||||
Future<void> _savePassage() async {
|
||||
if (_isSubmitting) return;
|
||||
debugPrint('🟢 [SAVE] Début _savePassage');
|
||||
|
||||
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) {
|
||||
@@ -380,6 +439,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
// 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;
|
||||
@@ -397,6 +457,31 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
// 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');
|
||||
}
|
||||
} else {
|
||||
// Modification : conserver les coordonnées existantes
|
||||
finalGpsLat = widget.passage!.gpsLat;
|
||||
finalGpsLng = widget.passage!.gpsLng;
|
||||
}
|
||||
|
||||
final passageData = widget.passage?.copyWith(
|
||||
fkType: _selectedPassageType!,
|
||||
@@ -422,7 +507,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
PassageModel(
|
||||
id: 0, // Nouveau passage
|
||||
fkOperation: currentOperation!.id, // Opération active
|
||||
fkSector: 0, // Secteur par défaut
|
||||
fkSector: 0, // Secteur par défaut (sera déterminé par l'API)
|
||||
fkUser: currentUser.id, // Utilisateur actuel
|
||||
fkType: _selectedPassageType!,
|
||||
fkAdresse: "0", // Adresse par défaut pour nouveau passage
|
||||
@@ -435,8 +520,8 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
fkHabitat: _fkHabitat,
|
||||
appt: _apptController.text.trim(),
|
||||
niveau: _niveauController.text.trim(),
|
||||
gpsLat: '0.0', // GPS par défaut
|
||||
gpsLng: '0.0', // GPS par défaut
|
||||
gpsLat: finalGpsLat, // GPS de l'utilisateur ou 0.0 si non disponible
|
||||
gpsLng: finalGpsLng, // GPS de l'utilisateur ou 0.0 si non disponible
|
||||
nomRecu: _nameController.text.trim(),
|
||||
remarque: _remarqueController.text.trim(),
|
||||
montant: finalMontant,
|
||||
@@ -453,32 +538,37 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
);
|
||||
|
||||
// Sauvegarder le passage d'abord
|
||||
debugPrint('🟢 [SAVE] Préparation sauvegarde 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
|
||||
final success = await widget.passageRepository.updatePassage(passageData);
|
||||
if (success) {
|
||||
savedPassage = passageData;
|
||||
}
|
||||
}
|
||||
|
||||
if (savedPassage == null) {
|
||||
throw Exception(widget.passage == null || widget.passage!.id == 0
|
||||
? "Échec de la création du passage"
|
||||
: "Échec de la mise à jour du passage");
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -514,17 +604,30 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
// Lancer le flow Tap to Pay
|
||||
final paymentSuccess = await _attemptTapToPayWithPassage(confirmedPassage, montant);
|
||||
|
||||
if (!paymentSuccess) {
|
||||
debugPrint('⚠️ Paiement Tap to Pay échoué pour le passage ${confirmedPassage.id}');
|
||||
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
|
||||
}
|
||||
},
|
||||
onQRCodeCompleted: () {
|
||||
// Pour QR Code: fermer le formulaire après l'affichage du QR
|
||||
if (mounted) {
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
widget.onSuccess?.call();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Fermer le formulaire après le choix de paiement
|
||||
if (mounted) {
|
||||
Navigator.of(context, rootNavigator: false).pop();
|
||||
widget.onSuccess?.call();
|
||||
}
|
||||
// NOTE: Le formulaire n'est plus fermé systématiquement ici
|
||||
// Il est fermé dans onQRCodeCompleted pour QR Code
|
||||
// ou dans onTapToPaySelected en cas de succès Tap to Pay
|
||||
}
|
||||
} else {
|
||||
// Stripe non activé pour cette amicale
|
||||
@@ -563,30 +666,44 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
} 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');
|
||||
|
||||
LoadingSpinOverlayUtils.hideSpecific(overlay);
|
||||
|
||||
// Afficher l'erreur
|
||||
final errorMessage = ApiException.fromError(e).message;
|
||||
debugPrint('❌ [SAVE] Message d\'erreur formaté: $errorMessage');
|
||||
|
||||
if (mounted) {
|
||||
await ResultDialog.show(
|
||||
context: context,
|
||||
success: false,
|
||||
message: ApiException.fromError(e).message,
|
||||
message: errorMessage,
|
||||
);
|
||||
}
|
||||
} 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());
|
||||
@@ -596,20 +713,28 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
await _settingsBox.put('lastPassageAppt', _apptController.text.trim());
|
||||
await _settingsBox.put('lastPassageNiveau', _niveauController.text.trim());
|
||||
|
||||
debugPrint('✅ Adresse mémorisée pour la prochaine création de passage');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur lors de la mémorisation de l\'adresse: $e');
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
@@ -625,6 +750,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
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
|
||||
final updatedPassage = passage.copyWith(residence: residence);
|
||||
await passagesBox.put(passage.key, updatedPassage);
|
||||
@@ -634,10 +760,13 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
|
||||
}
|
||||
|
||||
if (updatedCount > 0) {
|
||||
debugPrint('✅ Résidence propagée à $updatedCount passage(s) de l\'immeuble');
|
||||
debugPrint('✅ [PROPAGATE] Résidence propagée à $updatedCount passage(s)');
|
||||
} else {
|
||||
debugPrint('✅ [PROPAGATE] Aucun passage à mettre à jour');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur lors de la propagation de la résidence: $e');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('❌ [PROPAGATE] Erreur lors de la propagation: $e');
|
||||
debugPrint('❌ [PROPAGATE] Stack trace:\n$stackTrace');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1819,9 +1948,46 @@ class _TapToPayFlowDialogState extends State<_TapToPayFlowDialog> {
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
// Analyser le type d'erreur pour afficher un message clair
|
||||
final errorMsg = e.toString().toLowerCase();
|
||||
|
||||
String userMessage;
|
||||
bool shouldCancelPayment = true;
|
||||
|
||||
if (errorMsg.contains('canceled') || errorMsg.contains('cancelled')) {
|
||||
// Annulation volontaire par l'utilisateur
|
||||
userMessage = 'Paiement annulé';
|
||||
|
||||
} else if (errorMsg.contains('cardreadtimedout') || errorMsg.contains('timed out')) {
|
||||
// Timeout de lecture NFC
|
||||
userMessage = 'Lecture de la carte impossible.\n\n'
|
||||
'Conseils :\n'
|
||||
'• Maintenez la carte contre le dos du téléphone\n'
|
||||
'• Ne bougez pas jusqu\'à confirmation\n'
|
||||
'• Retirez la coque si nécessaire\n'
|
||||
'• Essayez différentes positions sur le téléphone';
|
||||
|
||||
} else if (errorMsg.contains('already') && errorMsg.contains('payment')) {
|
||||
// PaymentIntent existe déjà
|
||||
userMessage = 'Un paiement est déjà en cours pour ce passage.\n'
|
||||
'Veuillez réessayer dans quelques instants.';
|
||||
shouldCancelPayment = false; // Déjà en cours, pas besoin d'annuler
|
||||
|
||||
} else {
|
||||
// Autre erreur technique
|
||||
userMessage = 'Erreur lors du paiement.\n\n$e';
|
||||
}
|
||||
|
||||
// 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');
|
||||
});
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_currentState = 'error';
|
||||
_errorMessage = e.toString();
|
||||
_errorMessage = userMessage;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,14 @@ import 'package:geosector_app/presentation/widgets/qr_code_payment_dialog.dart';
|
||||
|
||||
/// Dialog de sélection de la méthode de paiement CB
|
||||
/// Affiche les options QR Code et/ou Tap to Pay selon la compatibilité
|
||||
class PaymentMethodSelectionDialog extends StatelessWidget {
|
||||
class PaymentMethodSelectionDialog extends StatefulWidget {
|
||||
final PassageModel passage;
|
||||
final double amount;
|
||||
final String habitantName;
|
||||
final StripeConnectService stripeConnectService;
|
||||
final PassageRepository? passageRepository;
|
||||
final VoidCallback? onTapToPaySelected;
|
||||
final VoidCallback? onQRCodeCompleted;
|
||||
|
||||
const PaymentMethodSelectionDialog({
|
||||
super.key,
|
||||
@@ -24,12 +25,61 @@ class PaymentMethodSelectionDialog extends StatelessWidget {
|
||||
required this.stripeConnectService,
|
||||
this.passageRepository,
|
||||
this.onTapToPaySelected,
|
||||
this.onQRCodeCompleted,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PaymentMethodSelectionDialog> createState() => _PaymentMethodSelectionDialogState();
|
||||
|
||||
/// Afficher le dialog de sélection de méthode de paiement
|
||||
static Future<void> show({
|
||||
required BuildContext context,
|
||||
required PassageModel passage,
|
||||
required double amount,
|
||||
required String habitantName,
|
||||
required StripeConnectService stripeConnectService,
|
||||
PassageRepository? passageRepository,
|
||||
VoidCallback? onTapToPaySelected,
|
||||
VoidCallback? onQRCodeCompleted,
|
||||
}) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false, // Ne peut pas fermer en cliquant à côté
|
||||
builder: (context) => PaymentMethodSelectionDialog(
|
||||
passage: passage,
|
||||
amount: amount,
|
||||
habitantName: habitantName,
|
||||
stripeConnectService: stripeConnectService,
|
||||
passageRepository: passageRepository,
|
||||
onTapToPaySelected: onTapToPaySelected,
|
||||
onQRCodeCompleted: onQRCodeCompleted,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PaymentMethodSelectionDialogState extends State<PaymentMethodSelectionDialog> {
|
||||
String? _tapToPayUnavailableReason;
|
||||
bool _isCheckingNFC = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkTapToPayAvailability();
|
||||
}
|
||||
|
||||
Future<void> _checkTapToPayAvailability() async {
|
||||
final reason = await DeviceInfoService.instance.getTapToPayUnavailableReasonAsync();
|
||||
setState(() {
|
||||
_tapToPayUnavailableReason = reason;
|
||||
_isCheckingNFC = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canUseTapToPay = DeviceInfoService.instance.canUseTapToPay();
|
||||
final amountEuros = amount.toStringAsFixed(2);
|
||||
final canUseTapToPay = !_isCheckingNFC && _tapToPayUnavailableReason == null;
|
||||
final amountEuros = widget.amount.toStringAsFixed(2);
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -42,21 +92,12 @@ class PaymentMethodSelectionDialog extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// En-tête
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Règlement CB',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
const Text(
|
||||
'Règlement CB',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@@ -76,7 +117,7 @@ class PaymentMethodSelectionDialog extends StatelessWidget {
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
habitantName,
|
||||
widget.habitantName,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -128,23 +169,28 @@ class PaymentMethodSelectionDialog extends StatelessWidget {
|
||||
description: 'Le client scanne le code avec son téléphone',
|
||||
onPressed: () => _handleQRCodePayment(context),
|
||||
color: Colors.blue,
|
||||
isEnabled: true,
|
||||
),
|
||||
|
||||
if (canUseTapToPay) ...[
|
||||
const SizedBox(height: 12),
|
||||
// Bouton Tap to Pay
|
||||
_buildPaymentButton(
|
||||
context: context,
|
||||
icon: Icons.contactless,
|
||||
label: 'Tap to Pay',
|
||||
description: 'Paiement sans contact sur cet appareil',
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onTapToPaySelected?.call();
|
||||
},
|
||||
color: Colors.green,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Bouton Tap to Pay (toujours affiché, désactivé si non disponible)
|
||||
_buildPaymentButton(
|
||||
context: context,
|
||||
icon: Icons.contactless,
|
||||
label: _isCheckingNFC ? 'Tap to Pay (vérification...)' : 'Tap to Pay',
|
||||
description: canUseTapToPay
|
||||
? 'Paiement sans contact sur cet appareil'
|
||||
: _tapToPayUnavailableReason ?? 'Vérification en cours...',
|
||||
onPressed: canUseTapToPay
|
||||
? () {
|
||||
Navigator.of(context).pop();
|
||||
widget.onTapToPaySelected?.call();
|
||||
}
|
||||
: null,
|
||||
color: Colors.green,
|
||||
isEnabled: canUseTapToPay,
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@@ -179,18 +225,24 @@ class PaymentMethodSelectionDialog extends StatelessWidget {
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String description,
|
||||
required VoidCallback onPressed,
|
||||
required VoidCallback? onPressed,
|
||||
required Color color,
|
||||
required bool isEnabled,
|
||||
}) {
|
||||
// Couleurs selon l'état activé/désactivé
|
||||
final effectiveColor = isEnabled ? color : Colors.grey;
|
||||
final backgroundColor = isEnabled ? color.withOpacity(0.1) : Colors.grey.shade100;
|
||||
final borderColor = isEnabled ? color.withOpacity(0.3) : Colors.grey.shade300;
|
||||
|
||||
return InkWell(
|
||||
onTap: onPressed,
|
||||
onTap: isEnabled ? onPressed : null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
border: Border.all(color: color.withOpacity(0.3), width: 2),
|
||||
color: backgroundColor,
|
||||
border: Border.all(color: borderColor, width: 2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
@@ -198,36 +250,69 @@ class PaymentMethodSelectionDialog extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.2),
|
||||
color: isEnabled ? color.withOpacity(0.2) : Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 32),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: effectiveColor,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: effectiveColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isEnabled)
|
||||
Icon(
|
||||
Icons.lock_outline,
|
||||
color: Colors.grey.shade600,
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
description,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isEnabled) ...[
|
||||
Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
color: Colors.orange.shade700,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
description,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: isEnabled ? Colors.grey.shade700 : Colors.orange.shade700,
|
||||
fontWeight: isEnabled ? FontWeight.normal : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.arrow_forward_ios, color: color, size: 20),
|
||||
if (isEnabled)
|
||||
Icon(Icons.arrow_forward_ios, color: effectiveColor, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -238,6 +323,7 @@ class PaymentMethodSelectionDialog extends StatelessWidget {
|
||||
Future<void> _handleQRCodePayment(BuildContext context) async {
|
||||
// Sauvegarder le navigator avant de fermer les dialogs
|
||||
final navigator = Navigator.of(context);
|
||||
bool loaderDisplayed = false;
|
||||
|
||||
try {
|
||||
// Afficher un loader
|
||||
@@ -248,19 +334,20 @@ class PaymentMethodSelectionDialog extends StatelessWidget {
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
loaderDisplayed = true;
|
||||
|
||||
// Créer le Payment Link
|
||||
final amountInCents = (amount * 100).round();
|
||||
debugPrint('🔵 Création Payment Link : ${amountInCents} cents, passage ${passage.id}');
|
||||
final amountInCents = (widget.amount * 100).round();
|
||||
debugPrint('🔵 Création Payment Link : ${amountInCents} cents, passage ${widget.passage.id}');
|
||||
|
||||
final paymentLink = await stripeConnectService.createPaymentLink(
|
||||
final paymentLink = await widget.stripeConnectService.createPaymentLink(
|
||||
amountInCents: amountInCents,
|
||||
passageId: passage.id,
|
||||
description: 'Calendrier pompiers - ${habitantName}',
|
||||
passageId: widget.passage.id,
|
||||
description: 'Calendrier pompiers - ${widget.habitantName}',
|
||||
metadata: {
|
||||
'passage_id': passage.id.toString(),
|
||||
'habitant_name': habitantName,
|
||||
'adresse': '${passage.numero} ${passage.rue}, ${passage.ville}',
|
||||
'passage_id': widget.passage.id.toString(),
|
||||
'habitant_name': widget.habitantName,
|
||||
'adresse': '${widget.passage.numero} ${widget.passage.rue}, ${widget.passage.ville}',
|
||||
},
|
||||
);
|
||||
|
||||
@@ -270,22 +357,18 @@ class PaymentMethodSelectionDialog extends StatelessWidget {
|
||||
debugPrint(' ID: ${paymentLink.paymentLinkId}');
|
||||
}
|
||||
|
||||
// Fermer le loader
|
||||
navigator.pop();
|
||||
debugPrint('🔵 Loader fermé');
|
||||
|
||||
if (paymentLink == null) {
|
||||
throw Exception('Impossible de créer le lien de paiement');
|
||||
}
|
||||
|
||||
// Sauvegarder l'URL du Payment Link dans le passage
|
||||
if (passageRepository != null) {
|
||||
if (widget.passageRepository != null) {
|
||||
try {
|
||||
debugPrint('🔵 Sauvegarde de l\'URL du Payment Link dans le passage...');
|
||||
final updatedPassage = passage.copyWith(
|
||||
final updatedPassage = widget.passage.copyWith(
|
||||
stripePaymentLinkUrl: paymentLink.url,
|
||||
);
|
||||
await passageRepository!.updatePassage(updatedPassage);
|
||||
await widget.passageRepository!.updatePassage(updatedPassage);
|
||||
debugPrint('✅ URL du Payment Link sauvegardée');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur lors de la sauvegarde de l\'URL: $e');
|
||||
@@ -293,7 +376,12 @@ class PaymentMethodSelectionDialog extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// Fermer le dialog de sélection
|
||||
// Fermer le loader
|
||||
navigator.pop();
|
||||
loaderDisplayed = false;
|
||||
debugPrint('🔵 Loader fermé');
|
||||
|
||||
// Fermer le dialog de sélection (seulement en cas de succès)
|
||||
navigator.pop();
|
||||
debugPrint('🔵 Dialog de sélection fermé');
|
||||
|
||||
@@ -311,43 +399,25 @@ class PaymentMethodSelectionDialog extends StatelessWidget {
|
||||
);
|
||||
debugPrint('🔵 Dialog QR Code affiché');
|
||||
|
||||
// Notifier que le QR Code est complété
|
||||
widget.onQRCodeCompleted?.call();
|
||||
debugPrint('✅ Callback onQRCodeCompleted appelé');
|
||||
|
||||
} catch (e, stack) {
|
||||
debugPrint('❌ Erreur dans _handleQRCodePayment: $e');
|
||||
debugPrint(' Stack: $stack');
|
||||
|
||||
// Fermer le loader si encore ouvert
|
||||
try {
|
||||
navigator.pop();
|
||||
} catch (_) {}
|
||||
if (loaderDisplayed) {
|
||||
try {
|
||||
navigator.pop();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Afficher l'erreur
|
||||
// Afficher l'erreur (le dialogue de sélection reste ouvert)
|
||||
if (context.mounted) {
|
||||
ApiException.showError(context, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Afficher le dialog de sélection de méthode de paiement
|
||||
static Future<void> show({
|
||||
required BuildContext context,
|
||||
required PassageModel passage,
|
||||
required double amount,
|
||||
required String habitantName,
|
||||
required StripeConnectService stripeConnectService,
|
||||
PassageRepository? passageRepository,
|
||||
VoidCallback? onTapToPaySelected,
|
||||
}) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (context) => PaymentMethodSelectionDialog(
|
||||
passage: passage,
|
||||
amount: amount,
|
||||
habitantName: habitantName,
|
||||
stripeConnectService: stripeConnectService,
|
||||
passageRepository: passageRepository,
|
||||
onTapToPaySelected: onTapToPaySelected,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,9 +816,9 @@ class _UserFormState extends State<UserForm> {
|
||||
},
|
||||
tooltip: _obscurePassword ? "Afficher le mot de passe" : "Masquer le mot de passe",
|
||||
),
|
||||
helperText: widget.user?.id != 0
|
||||
? "Laissez vide pour conserver le mot de passe actuel"
|
||||
: "8 à 64 caractères. Phrases de passe recommandées (ex: Mon chat Félix a 3 ans!)",
|
||||
helperText: widget.user?.id != 0
|
||||
? "Laissez vide pour conserver le mot de passe actuel"
|
||||
: "8 à 64 caractères, alphanumériques et caractères spéciaux",
|
||||
helperMaxLines: 3,
|
||||
validator: _validatePassword,
|
||||
),
|
||||
@@ -895,9 +895,9 @@ class _UserFormState extends State<UserForm> {
|
||||
},
|
||||
tooltip: _obscurePassword ? "Afficher le mot de passe" : "Masquer le mot de passe",
|
||||
),
|
||||
helperText: widget.user?.id != 0
|
||||
? "Laissez vide pour conserver le mot de passe actuel"
|
||||
: "8 à 64 caractères. Phrases de passe recommandées (ex: Mon chat Félix a 3 ans!)",
|
||||
helperText: widget.user?.id != 0
|
||||
? "Laissez vide pour conserver le mot de passe actuel"
|
||||
: "8 à 64 caractères, alphanumériques et caractères spéciaux",
|
||||
helperMaxLines: 3,
|
||||
validator: _validatePassword,
|
||||
),
|
||||
|
||||
@@ -240,7 +240,7 @@ class _UserFormDialogState extends State<UserFormDialog> {
|
||||
user: widget.user,
|
||||
readOnly: widget.readOnly,
|
||||
allowUsernameEdit: widget.allowUsernameEdit,
|
||||
allowSectNameEdit: widget.allowUsernameEdit,
|
||||
allowSectNameEdit: widget.isAdmin, // Toujours éditable pour un admin
|
||||
amicale: widget.amicale, // Passer l'amicale
|
||||
isAdmin: widget.isAdmin, // Passer isAdmin
|
||||
onSubmit: null, // Pas besoin de callback
|
||||
|
||||
Reference in New Issue
Block a user