Fix: Hive sync et update entité via API REST

- Correction mapping JSON membres (fk_role, chk_active)
- Ajout traitement amicale au login
- Fix callback onSubmit pour sync Hive après update API
This commit is contained in:
d6soft
2025-06-09 18:46:49 +02:00
parent 150016d772
commit 511be5a535
50 changed files with 71865 additions and 71984 deletions

View File

@@ -98,14 +98,23 @@ class _AmicaleFormState extends State<AmicaleForm> {
// Appeler l'API pour mettre à jour l'entité
Future<void> _updateAmicale(AmicaleModel amicale) async {
if (!mounted) return;
try {
// Afficher un indicateur de chargement
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
return const AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Mise à jour en cours...'),
],
),
);
},
);
@@ -121,9 +130,9 @@ class _AmicaleFormState extends State<AmicaleForm> {
'phone': amicale.phone,
'mobile': amicale.mobile,
'email': amicale.email,
'chk_copie_mail_recu': amicale.chkCopieMailRecu,
'chk_accept_sms': amicale.chkAcceptSms,
'chk_stripe': amicale.chkStripe,
'chk_copie_mail_recu': amicale.chkCopieMailRecu ? 1 : 0,
'chk_accept_sms': amicale.chkAcceptSms ? 1 : 0,
'chk_stripe': amicale.chkStripe ? 1 : 0,
};
// Ajouter les champs réservés aux administrateurs si l'utilisateur est admin
@@ -132,63 +141,81 @@ class _AmicaleFormState extends State<AmicaleForm> {
data['gps_lat'] = amicale.gpsLat;
data['gps_lng'] = amicale.gpsLng;
data['stripe_id'] = amicale.stripeId;
data['chk_demo'] = amicale.chkDemo;
data['chk_active'] = amicale.chkActive;
data['chk_demo'] = amicale.chkDemo ? 1 : 0;
data['chk_active'] = amicale.chkActive ? 1 : 0;
}
// Fermer l'indicateur de chargement
Navigator.of(context).pop();
debugPrint('🔧 Données à envoyer à l\'API: $data');
bool apiSuccess = false;
String? errorMessage;
// Appeler l'API si le service est disponible
if (widget.apiService != null) {
try {
await widget.apiService!.post('/entite/update', data: data);
debugPrint('📡 Appel API pour mise à jour amicale...');
// Afficher un message de succès
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Amicale mise à jour avec succès'),
backgroundColor: Colors.green,
),
);
// Version RESTful correcte avec PUT
final response = await widget.apiService!.put('/entites/${amicale.id}', data: data);
// Alternative avec PATCH si votre API le supporte
// final response = await widget.apiService!.patch('/entites/${amicale.id}', data: data);
debugPrint('📡 Réponse API: ${response.statusCode}');
if (response.statusCode == 200 || response.statusCode == 201) {
apiSuccess = true;
} else {
errorMessage = 'Erreur serveur: ${response.statusCode}';
}
} catch (error) {
// Afficher un message d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la mise à jour de l\'amicale: $error'),
backgroundColor: Colors.red,
),
);
}
return; // Sortir de la fonction en cas d'erreur
}
} else {
// Pas d'API service, afficher un message d'information
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Modifications enregistrées localement'),
backgroundColor: Colors.blue,
),
);
debugPrint('❌ Erreur API: $error');
errorMessage = 'Erreur lors de la communication avec le serveur: $error';
}
}
// Appeler la fonction onSubmit si elle existe
if (widget.onSubmit != null) {
widget.onSubmit!(amicale);
}
// Fermer le formulaire
if (mounted) {
// Fermer l'indicateur de chargement
if (mounted && Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
if (!mounted) return;
if (apiSuccess) {
// Appeler la fonction onSubmit si elle existe
if (widget.onSubmit != null) {
widget.onSubmit!(amicale);
}
// Afficher un message de succès
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(widget.apiService != null ? 'Amicale mise à jour avec succès' : 'Modifications enregistrées localement'),
backgroundColor: Colors.green,
),
);
// Fermer le formulaire après un délai pour que l'utilisateur voie le message
await Future.delayed(const Duration(milliseconds: 500));
if (mounted && Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
} else {
// Afficher un message d'erreur
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessage ?? 'Erreur lors de la mise à jour'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 4),
),
);
}
} catch (e) {
debugPrint('❌ Erreur générale dans _updateAmicale: $e');
// Fermer l'indicateur de chargement si encore ouvert
if (Navigator.of(context).canPop()) {
if (mounted && Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
@@ -196,8 +223,9 @@ class _AmicaleFormState extends State<AmicaleForm> {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: ${e.toString()}'),
content: Text('Erreur inattendue: ${e.toString()}'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 4),
),
);
}
@@ -205,9 +233,14 @@ class _AmicaleFormState extends State<AmicaleForm> {
}
void _submitForm() {
debugPrint('🔧 _submitForm appelée');
if (_formKey.currentState!.validate()) {
debugPrint('🔧 Formulaire valide');
// Vérifier qu'au moins un numéro de téléphone est renseigné
if (_phoneController.text.isEmpty && _mobileController.text.isEmpty) {
debugPrint('⚠️ Aucun numéro de téléphone renseigné');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez renseigner au moins un numéro de téléphone'),
@@ -217,6 +250,8 @@ class _AmicaleFormState extends State<AmicaleForm> {
return;
}
debugPrint('🔧 Création de l\'objet AmicaleModel...');
final amicale = widget.amicale?.copyWith(
name: _nameController.text,
adresse1: _adresse1Controller.text,
@@ -258,10 +293,13 @@ class _AmicaleFormState extends State<AmicaleForm> {
chkActive: _chkActive,
);
debugPrint('🔧 AmicaleModel créé: ${amicale.name}');
debugPrint('🔧 Appel de _updateAmicale...');
// Appeler l'API pour mettre à jour l'amicale
_updateAmicale(amicale);
// Ne pas appeler widget.onSubmit ici car c'est fait dans _updateAmicale
} else {
debugPrint('❌ Formulaire invalide');
}
}

View File

@@ -22,7 +22,7 @@ class AmicaleTableWidget extends StatelessWidget {
final Function(AmicaleModel)? onDelete;
final AmicaleRepository amicaleRepository;
final UserRepository userRepository; // Nouveau paramètre
final ApiService? apiService; // Nouveau paramètre optionnel
final ApiService? apiService;
final bool isLoading;
final String? emptyMessage;
final bool readOnly;
@@ -35,7 +35,7 @@ class AmicaleTableWidget extends StatelessWidget {
required this.userRepository, // Requis
this.onEdit,
this.onDelete,
this.apiService, // Optionnel
this.apiService,
this.isLoading = false,
this.emptyMessage,
this.readOnly = false,
@@ -83,9 +83,13 @@ class AmicaleTableWidget extends StatelessWidget {
readOnly: false,
userRepository: userRepository,
apiService: apiService,
onSubmit: (updatedAmicale) {
onSubmit: (updatedAmicale) async {
// Sauvegarder l'amicale mise à jour dans le repository
debugPrint('🔄 Sauvegarde de l\'amicale mise à jour: ${updatedAmicale.name}');
await amicaleRepository.saveAmicale(updatedAmicale);
debugPrint('✅ Amicale sauvegardée dans le repository');
Navigator.of(dialogContext).pop();
// La mise à jour sera gérée par les ValueListenableBuilder
},
),
),
@@ -227,7 +231,12 @@ class AmicaleTableWidget extends StatelessWidget {
readOnly: true,
userRepository: userRepository,
apiService: apiService,
onSubmit: (updatedAmicale) {
onSubmit: (updatedAmicale) async {
// Sauvegarder l'amicale mise à jour dans le repository
debugPrint('🔄 Sauvegarde de l\'amicale mise à jour: ${updatedAmicale.name}');
await amicaleRepository.saveAmicale(updatedAmicale);
debugPrint('✅ Amicale sauvegardée dans le repository');
Navigator.of(dialogContext).pop();
},
),

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/app.dart'; // Pour accéder à l'instance globale de ApiService
import 'package:geosector_app/core/services/api_service.dart'; // Import du service singleton
/// Widget de carte réutilisable utilisant Mapbox
///
@@ -105,11 +105,10 @@ class _MapboxMapState extends State<MapboxMap> {
Widget build(BuildContext context) {
// Déterminer l'URL du template de tuiles Mapbox
// Utiliser l'environnement actuel pour obtenir la bonne clé API
final String environment = apiService.getCurrentEnvironment();
final String environment = ApiService.instance.getCurrentEnvironment();
final String mapboxToken = AppKeys.getMapboxApiKey(environment);
final String mapStyle = widget.mapStyle ?? 'mapbox/streets-v11';
final String urlTemplate =
'https://api.mapbox.com/styles/v1/$mapStyle/tiles/256/{z}/{x}/{y}@2x?access_token=$mapboxToken';
final String urlTemplate = 'https://api.mapbox.com/styles/v1/$mapStyle/tiles/256/{z}/{x}/{y}@2x?access_token=$mapboxToken';
return Stack(
children: [
@@ -145,12 +144,10 @@ class _MapboxMapState extends State<MapboxMap> {
),
// Polygones
if (widget.polygons != null && widget.polygons!.isNotEmpty)
PolygonLayer(polygons: widget.polygons!),
if (widget.polygons != null && widget.polygons!.isNotEmpty) PolygonLayer(polygons: widget.polygons!),
// Marqueurs
if (widget.markers != null && widget.markers!.isNotEmpty)
MarkerLayer(markers: widget.markers!),
if (widget.markers != null && widget.markers!.isNotEmpty) MarkerLayer(markers: widget.markers!),
],
),

View File

@@ -44,7 +44,7 @@ class MembreRowWidget extends StatelessWidget {
Expanded(
flex: 2,
child: Text(
membre.firstName,
membre.firstName ?? '',
style: theme.textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
@@ -54,7 +54,7 @@ class MembreRowWidget extends StatelessWidget {
Expanded(
flex: 2,
child: Text(
membre.name,
membre.name ?? '',
style: theme.textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
@@ -70,31 +70,31 @@ class MembreRowWidget extends StatelessWidget {
),
),
// Rôle (fkRole)
// Rôle (role au lieu de fkRole)
Expanded(
flex: 1,
child: Text(
_getRoleName(membre.fkRole),
_getRoleName(membre.role),
style: theme.textTheme.bodyMedium,
),
),
// Statut (actif/inactif)
// Statut (isActive au lieu de chkActive)
Expanded(
flex: 1,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: membre.chkActive == 1 ? Colors.green.withOpacity(0.1) : Colors.red.withOpacity(0.1),
color: membre.isActive ? Colors.green.withOpacity(0.1) : Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: membre.chkActive == 1 ? Colors.green.withOpacity(0.3) : Colors.red.withOpacity(0.3),
color: membre.isActive ? Colors.green.withOpacity(0.3) : Colors.red.withOpacity(0.3),
),
),
child: Text(
membre.chkActive == 1 ? 'Actif' : 'Inactif',
membre.isActive ? 'Actif' : 'Inactif',
style: theme.textTheme.bodySmall?.copyWith(
color: membre.chkActive == 1 ? Colors.green[700] : Colors.red[700],
color: membre.isActive ? Colors.green[700] : Colors.red[700],
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
@@ -161,18 +161,20 @@ class MembreRowWidget extends StatelessWidget {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('${membre.firstName} ${membre.name}'),
title: Text('${membre.firstName ?? ''} ${membre.name ?? ''}'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow('ID', membre.id.toString()),
_buildDetailRow('Email', membre.email),
_buildDetailRow('Username', membre.username),
_buildDetailRow('Rôle', _getRoleName(membre.fkRole)),
_buildDetailRow('Titre', membre.fkTitre.toString()),
_buildDetailRow('Username', membre.username ?? 'Non défini'),
_buildDetailRow('Rôle', _getRoleName(membre.role)),
_buildDetailRow('Titre', membre.fkTitre?.toString() ?? 'Non défini'),
_buildDetailRow('Secteur', membre.sectName ?? 'Non défini'),
_buildDetailRow('Statut', membre.chkActive == 1 ? 'Actif' : 'Inactif'),
_buildDetailRow('Statut', membre.isActive ? 'Actif' : 'Inactif'),
_buildDetailRow('Téléphone', membre.phone ?? 'Non défini'),
_buildDetailRow('Mobile', membre.mobile ?? 'Non défini'),
if (membre.dateNaissance != null)
_buildDetailRow('Date de naissance', '${membre.dateNaissance!.day}/${membre.dateNaissance!.month}/${membre.dateNaissance!.year}'),
if (membre.dateEmbauche != null)