feat: création branche singletons - début refactorisation

- Sauvegarde des fichiers critiques
- Préparation transformation ApiService en singleton
- Préparation création CurrentUserService et CurrentAmicaleService
- Objectif: renommer Box users -> user
This commit is contained in:
d6soft
2025-06-05 15:22:29 +02:00
parent 2aa2706179
commit e5ab857913
48 changed files with 131679 additions and 128324 deletions

View File

@@ -6,20 +6,23 @@ import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/presentation/widgets/mapbox_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
import 'custom_text_field.dart';
class AmicaleForm extends StatefulWidget {
final AmicaleModel? amicale;
final Function(AmicaleModel)? onSubmit;
final bool readOnly;
final UserRepository userRepository; // Nouveau paramètre
final ApiService? apiService; // Nouveau paramètre optionnel
const AmicaleForm({
Key? key,
super.key,
this.amicale,
this.onSubmit,
this.readOnly = false,
}) : super(key: key);
required this.userRepository, // Requis
this.apiService, // Optionnel
});
@override
State<AmicaleForm> createState() => _AmicaleFormState();
@@ -59,8 +62,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
_nameController = TextEditingController(text: amicale?.name ?? '');
_adresse1Controller = TextEditingController(text: amicale?.adresse1 ?? '');
_adresse2Controller = TextEditingController(text: amicale?.adresse2 ?? '');
_codePostalController =
TextEditingController(text: amicale?.codePostal ?? '');
_codePostalController = TextEditingController(text: amicale?.codePostal ?? '');
_villeController = TextEditingController(text: amicale?.ville ?? '');
_phoneController = TextEditingController(text: amicale?.phone ?? '');
_mobileController = TextEditingController(text: amicale?.mobile ?? '');
@@ -125,9 +127,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
};
// Ajouter les champs réservés aux administrateurs si l'utilisateur est admin
final userRepository =
Provider.of<UserRepository>(context, listen: false);
final userRole = userRepository.getUserRole();
final userRole = widget.userRepository.getUserRole();
if (userRole > 2) {
data['gps_lat'] = amicale.gpsLat;
data['gps_lng'] = amicale.gpsLng;
@@ -136,56 +136,71 @@ class _AmicaleFormState extends State<AmicaleForm> {
data['chk_active'] = amicale.chkActive;
}
// Appeler l'API
try {
// Obtenir l'instance du service API
final apiService = Provider.of<ApiService>(context, listen: false);
// Fermer l'indicateur de chargement
Navigator.of(context).pop();
// Appeler la méthode post du service API
await apiService.post('/entite/update', data: data);
// Appeler l'API si le service est disponible
if (widget.apiService != null) {
try {
await widget.apiService!.post('/entite/update', data: data);
// Fermer l'indicateur de chargement
Navigator.of(context).pop();
// Afficher un message de succès
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Amicale mise à jour avec succès'),
backgroundColor: Colors.green,
),
);
// Appeler la fonction onSubmit si elle existe
if (widget.onSubmit != null) {
widget.onSubmit!(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,
),
);
}
} 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,
),
);
}
}
// Fermer le formulaire
Navigator.of(context).pop();
} catch (error) {
// Fermer l'indicateur de chargement
Navigator.of(context).pop();
// Appeler la fonction onSubmit si elle existe
if (widget.onSubmit != null) {
widget.onSubmit!(amicale);
}
// Afficher un message d'erreur
// Fermer le formulaire
if (mounted) {
Navigator.of(context).pop();
}
} catch (e) {
// Fermer l'indicateur de chargement si encore ouvert
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
// Afficher un message d'erreur
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('Erreur lors de la mise à jour de l\'amicale: $error'),
content: Text('Erreur: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
// Fermer l'indicateur de chargement
Navigator.of(context).pop();
// Afficher un message d'erreur
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
@@ -195,13 +210,13 @@ class _AmicaleFormState extends State<AmicaleForm> {
if (_phoneController.text.isEmpty && _mobileController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text('Veuillez renseigner au moins un numéro de téléphone'),
content: Text('Veuillez renseigner au moins un numéro de téléphone'),
backgroundColor: Colors.red,
),
);
return;
}
final amicale = widget.amicale?.copyWith(
name: _nameController.text,
adresse1: _adresse1Controller.text,
@@ -246,10 +261,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
// Appeler l'API pour mettre à jour l'amicale
_updateAmicale(amicale);
// Appeler la fonction onSubmit si elle existe (pour la compatibilité avec le code existant)
if (widget.onSubmit != null) {
widget.onSubmit!(amicale);
}
// Ne pas appeler widget.onSubmit ici car c'est fait dans _updateAmicale
}
}
@@ -293,8 +305,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
// TODO: Implémenter la sélection d'image
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Fonctionnalité de modification du logo à venir'),
content: Text('Fonctionnalité de modification du logo à venir'),
),
);
},
@@ -447,7 +458,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
child: Text(
label,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onBackground,
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
@@ -481,7 +492,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
"Adresse",
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onBackground,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
@@ -566,7 +577,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
"Région",
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
color: theme.colorScheme.onBackground,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
@@ -580,7 +591,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
"Contact",
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onBackground,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
@@ -657,7 +668,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
"Informations avancées",
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onBackground,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
@@ -671,8 +682,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
child: CustomTextField(
controller: _gpsLatController,
label: "GPS Latitude",
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
readOnly: restrictedFieldsReadOnly,
),
),
@@ -682,8 +692,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
child: CustomTextField(
controller: _gpsLngController,
label: "GPS Longitude",
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
readOnly: restrictedFieldsReadOnly,
),
),
@@ -749,7 +758,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
Text(
"Accepte les règlements en CB",
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onBackground,
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
@@ -760,8 +769,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
controller: _stripeIdController,
label: "ID Stripe Paiements CB",
readOnly: restrictedFieldsReadOnly,
helperText:
"Les règlements par CB sont taxés d'une commission de 1.4%",
helperText: "Les règlements par CB sont taxés d'une commission de 1.4%",
),
),
],
@@ -774,7 +782,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
"Options",
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onBackground,
color: theme.colorScheme.onSurface,
),
),
const SizedBox(height: 8),
@@ -849,8 +857,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF20335E),
side: const BorderSide(color: Color(0xFF20335E)),
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 16),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
@@ -871,8 +878,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF20335E),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 16),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50),
),
@@ -895,70 +901,73 @@ class _AmicaleFormState extends State<AmicaleForm> {
// Vérifier si les informations avancées doivent être affichées
bool _shouldShowAdvancedInfo() {
final userRepository = Provider.of<UserRepository>(context, listen: false);
final userRole = userRepository.getUserRole();
final userRole = widget.userRepository.getUserRole();
final bool canEditRestrictedFields = userRole > 2;
return canEditRestrictedFields ||
_gpsLatController.text.isNotEmpty ||
_gpsLngController.text.isNotEmpty ||
_stripeIdController.text.isNotEmpty;
return canEditRestrictedFields || _gpsLatController.text.isNotEmpty || _gpsLngController.text.isNotEmpty || _stripeIdController.text.isNotEmpty;
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final userRepository = Provider.of<UserRepository>(context, listen: false);
final userRole = userRepository.getUserRole();
final userRole = widget.userRepository.getUserRole();
// Déterminer si l'utilisateur peut modifier les champs restreints
final bool canEditRestrictedFields = userRole > 2;
// Lecture seule pour les champs restreints si l'utilisateur n'a pas les droits
final bool restrictedFieldsReadOnly =
widget.readOnly || !canEditRestrictedFields;
final bool restrictedFieldsReadOnly = widget.readOnly || !canEditRestrictedFields;
// Calculer la largeur maximale du formulaire pour les écrans larges
final screenWidth = MediaQuery.of(context).size.width;
final maxFormWidth = screenWidth > 800 ? 800.0 : screenWidth;
return Scaffold(
appBar: AppBar(
title: Text(
widget.readOnly ? 'Détails de l\'amicale' : 'Modifier l\'amicale'),
backgroundColor: theme.appBarTheme.backgroundColor,
foregroundColor: theme.appBarTheme.foregroundColor,
),
body: Center(
child: Container(
width: maxFormWidth,
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
final formContent = Container(
width: maxFormWidth,
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header avec logo et minimap
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Header avec logo et minimap
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Section Logo
_buildLogoSection(),
// Section MiniMap
_buildMiniMap(),
],
),
const SizedBox(height: 24),
// Formulaire principal
_buildMainForm(theme, restrictedFieldsReadOnly),
// Section Logo
_buildLogoSection(),
// Section MiniMap
_buildMiniMap(),
],
),
),
const SizedBox(height: 24),
// Formulaire principal
_buildMainForm(theme, restrictedFieldsReadOnly),
],
),
),
),
);
// Vérifier si on est dans une Dialog en regardant le type du widget parent
final route = ModalRoute.of(context);
final isInDialog = route?.settings.name == null;
// Si on est dans une Dialog, ne pas utiliser Scaffold
if (isInDialog) {
return Center(child: formContent);
}
// Sinon, utiliser Scaffold pour les pages complètes
return Scaffold(
appBar: AppBar(
title: Text(widget.readOnly ? 'Détails de l\'amicale' : 'Modifier l\'amicale'),
backgroundColor: theme.appBarTheme.backgroundColor,
foregroundColor: theme.appBarTheme.foregroundColor,
),
body: Center(child: formContent),
);
}
}

View File

@@ -1,31 +1,33 @@
import 'package:flutter/material.dart';
import 'package:geosector_app/app.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
/// Widget pour afficher une ligne du tableau d'amicales
/// Affiche les colonnes id, name, codePostal, libRegion et une colonne Actions
/// La colonne Actions contient un bouton Delete pour les utilisateurs avec rôle > 2
/// La ligne entière est cliquable pour afficher les détails de l'amicale
/// Affiche les colonnes id, name, codePostal, libRegion et une colonne Actions (conditionnelle)
/// La colonne Actions contient des boutons Edit et Delete selon les permissions
/// Pour un admin d'amicale (rôle 2), seule la ligne est cliquable sans colonne Actions
class AmicaleRowWidget extends StatelessWidget {
final AmicaleModel amicale;
final Function(AmicaleModel)? onTap;
final Function(AmicaleModel)? onEdit;
final Function(AmicaleModel)? onDelete;
final bool isHeader;
final bool isAlternate;
final bool showActionsColumn;
const AmicaleRowWidget({
Key? key,
super.key,
required this.amicale,
this.onTap,
this.onEdit,
this.onDelete,
this.isHeader = false,
this.isAlternate = false,
}) : super(key: key);
this.showActionsColumn = true,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final userRole = userRepository.getUserRole();
// Définir les styles en fonction du type de ligne (en-tête ou données)
final textStyle = isHeader
@@ -36,11 +38,7 @@ class AmicaleRowWidget extends StatelessWidget {
: theme.textTheme.bodyMedium;
// Couleur de fond en fonction du type de ligne
final backgroundColor = isHeader
? theme.colorScheme.primary.withOpacity(0.1)
: (isAlternate
? theme.colorScheme.surface
: theme.colorScheme.background);
final backgroundColor = isHeader ? theme.colorScheme.primary.withOpacity(0.1) : (isAlternate ? theme.colorScheme.surface : theme.colorScheme.surface);
return InkWell(
onTap: isHeader || onTap == null ? null : () => onTap!(amicale),
@@ -55,7 +53,7 @@ class AmicaleRowWidget extends StatelessWidget {
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Row(
children: [
// Colonne ID
@@ -103,7 +101,7 @@ class AmicaleRowWidget extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
isHeader ? 'Ville' : (amicale.ville ?? ''),
isHeader ? 'Ville' : amicale.ville,
style: textStyle,
overflow: TextOverflow.ellipsis,
),
@@ -123,8 +121,8 @@ class AmicaleRowWidget extends StatelessWidget {
),
),
// Colonne Actions - seulement si l'utilisateur a le rôle > 2 et onDelete n'est pas null
if (isHeader || (userRole > 2 && onDelete != null))
// Colonne Actions (conditionnelle)
if (showActionsColumn && (isHeader || onEdit != null || onDelete != null))
Expanded(
flex: 2,
child: Padding(
@@ -138,22 +136,40 @@ class AmicaleRowWidget extends StatelessWidget {
: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
// Bouton Edit
if (onEdit != null)
IconButton(
icon: Icon(
Icons.edit,
color: theme.colorScheme.primary,
size: 20,
),
tooltip: 'Modifier',
onPressed: () => onEdit!(amicale),
constraints: const BoxConstraints(
minWidth: 36,
minHeight: 36,
),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
),
// Bouton Delete
IconButton(
icon: Icon(
Icons.delete,
color: theme.colorScheme.error,
size: 20,
if (onDelete != null)
IconButton(
icon: Icon(
Icons.delete,
color: theme.colorScheme.error,
size: 20,
),
tooltip: 'Supprimer',
onPressed: () => onDelete!(amicale),
constraints: const BoxConstraints(
minWidth: 36,
minHeight: 36,
),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
),
tooltip: 'Supprimer',
onPressed: () => onDelete!(amicale),
constraints: const BoxConstraints(
minWidth: 36,
minHeight: 36,
),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
),
],
),
),

View File

@@ -1,11 +1,10 @@
import 'package:flutter/material.dart';
import 'package:geosector_app/app.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/repositories/region_repository.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/core/repositories/amicale_repository.dart';
import 'package:geosector_app/presentation/widgets/amicale_row_widget.dart';
import 'package:geosector_app/presentation/widgets/amicale_form.dart';
import 'package:provider/provider.dart';
/// Widget de tableau pour afficher une liste d'amicales
///
@@ -19,19 +18,83 @@ import 'package:provider/provider.dart';
/// Lorsqu'on clique sur une ligne, une modale s'affiche avec le formulaire EntiteForm
class AmicaleTableWidget extends StatelessWidget {
final List<AmicaleModel> amicales;
final Function(AmicaleModel)? onEdit;
final Function(AmicaleModel)? onDelete;
final AmicaleRepository amicaleRepository;
final UserRepository userRepository; // Nouveau paramètre
final ApiService? apiService; // Nouveau paramètre optionnel
final bool isLoading;
final String? emptyMessage;
final bool readOnly;
final bool showActionsColumn;
const AmicaleTableWidget({
Key? key,
super.key,
required this.amicales,
required this.amicaleRepository,
required this.userRepository, // Requis
this.onEdit,
this.onDelete,
this.apiService, // Optionnel
this.isLoading = false,
this.emptyMessage,
this.readOnly = false,
}) : super(key: key);
this.showActionsColumn = true,
});
// Ajouter cette nouvelle méthode pour ouvrir directement le formulaire d'édition :
void _showAmicaleEditForm(BuildContext context, AmicaleModel amicale) {
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
width: MediaQuery.of(dialogContext).size.width * 0.9,
height: MediaQuery.of(dialogContext).size.height * 0.9,
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Header de la dialog
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Modifier l\'amicale',
style: Theme.of(dialogContext).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(dialogContext).colorScheme.primary,
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(dialogContext).pop(),
),
],
),
const Divider(),
// Contenu du formulaire
Expanded(
child: AmicaleForm(
amicale: amicale,
readOnly: false,
userRepository: userRepository,
apiService: apiService,
onSubmit: (updatedAmicale) {
Navigator.of(dialogContext).pop();
// La mise à jour sera gérée par les ValueListenableBuilder
},
),
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
@@ -51,7 +114,9 @@ class AmicaleTableWidget extends StatelessWidget {
),
isHeader: true,
onTap: null,
onEdit: null,
onDelete: null,
showActionsColumn: showActionsColumn,
),
// Corps du tableau
@@ -90,8 +155,7 @@ class AmicaleTableWidget extends StatelessWidget {
child: Text(
emptyMessage ?? 'Aucune amicale trouvée',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color:
Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
),
@@ -107,10 +171,18 @@ class AmicaleTableWidget extends StatelessWidget {
final amicale = amicales[index];
return AmicaleRowWidget(
amicale: amicale,
isAlternate: index % 2 == 1, // Alterner les couleurs
onTap: (selectedAmicale) =>
_showAmicaleDetails(context, selectedAmicale),
isAlternate: index % 2 == 1,
onTap: (selectedAmicale) {
// Si pas de colonne Actions, ouvrir directement le formulaire d'édition
if (!showActionsColumn) {
_showAmicaleEditForm(context, selectedAmicale);
} else {
_showAmicaleDetails(context, selectedAmicale);
}
},
onEdit: onEdit,
onDelete: onDelete,
showActionsColumn: showActionsColumn,
);
},
);
@@ -118,63 +190,48 @@ class AmicaleTableWidget extends StatelessWidget {
// Afficher une modale avec le formulaire EntiteForm
void _showAmicaleDetails(BuildContext context, AmicaleModel amicale) {
// Utiliser l'instance globale de userRepository définie dans app.dart
final userRepo = userRepository;
// Créer une instance de RegionRepository
final regionRepo = RegionRepository();
showDialog(
context: context,
builder: (dialogContext) => MultiProvider(
providers: [
// Fournir les repositories nécessaires au formulaire
Provider<UserRepository>.value(value: userRepo),
Provider<RegionRepository>.value(value: regionRepo),
],
child: Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
width: MediaQuery.of(dialogContext).size.width * 0.6,
padding: const EdgeInsets.all(24),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Détails de l\'amicale',
style: Theme.of(dialogContext)
.textTheme
.headlineSmall
?.copyWith(
fontWeight: FontWeight.bold,
color:
Theme.of(dialogContext).colorScheme.primary,
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(dialogContext).pop(),
),
],
),
const SizedBox(height: 16),
// Formulaire EntiteForm en mode lecture seule
AmicaleForm(
amicale: amicale,
readOnly: readOnly,
onSubmit: (updatedAmicale) {
Navigator.of(dialogContext).pop();
// Ici, vous pourriez ajouter une logique pour mettre à jour l'amicale
},
),
],
),
builder: (dialogContext) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
width: MediaQuery.of(dialogContext).size.width * 0.6,
padding: const EdgeInsets.all(24),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Détails de l\'amicale',
style: Theme.of(dialogContext).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(dialogContext).colorScheme.primary,
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(dialogContext).pop(),
),
],
),
const SizedBox(height: 16),
// Formulaire AmicaleForm en mode lecture seule
AmicaleForm(
amicale: amicale,
readOnly: true,
userRepository: userRepository,
apiService: apiService,
onSubmit: (updatedAmicale) {
Navigator.of(dialogContext).pop();
},
),
],
),
),
),

View File

@@ -26,14 +26,14 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
final VoidCallback? onLogoutPressed;
const DashboardAppBar({
Key? key,
super.key,
required this.title,
this.pageTitle,
this.showNewPassageButton = true,
this.onNewPassagePressed,
this.isAdmin = false,
this.onLogoutPressed,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {
@@ -82,10 +82,12 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
),
);
actions.add(const SizedBox(width: 8));
// Ajouter la version de l'application
actions.add(
Text(
AppInfoService.fullVersion,
"v${AppInfoService.version}",
style: const TextStyle(
fontSize: 12,
color: Colors.white70,
@@ -93,11 +95,12 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
),
);
actions.add(const SizedBox(width: 8));
actions.add(
TextButton.icon(
icon: const Icon(Icons.add_location_alt, color: Colors.white),
label: const Text('Nouveau passage',
style: TextStyle(color: Colors.white)),
label: const Text('Nouveau passage', style: TextStyle(color: Colors.white)),
onPressed: onNewPassagePressed,
style: TextButton.styleFrom(
backgroundColor: theme.colorScheme.secondary,
@@ -106,6 +109,8 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
),
);
actions.add(const SizedBox(width: 8));
// Ajouter le bouton "Mon compte"
actions.add(
IconButton(
@@ -128,6 +133,8 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
),
);
actions.add(const SizedBox(width: 8));
// Ajouter le bouton de déconnexion
actions.add(
IconButton(
@@ -139,8 +146,7 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Déconnexion'),
content:
const Text('Voulez-vous vraiment vous déconnecter ?'),
content: const Text('Voulez-vous vraiment vous déconnecter ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
@@ -157,8 +163,7 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
// Vérification supplémentaire et navigation forcée si nécessaire
if (success && context.mounted) {
// Attendre un court instant pour que les changements d'état se propagent
await Future.delayed(
const Duration(milliseconds: 100));
await Future.delayed(const Duration(milliseconds: 100));
// Navigation forcée vers la page d'accueil
context.go('/');

View File

@@ -3,110 +3,207 @@ import 'package:geosector_app/core/data/models/membre_model.dart';
class MembreRowWidget extends StatelessWidget {
final MembreModel membre;
final Function()? onEdit;
final Function()? onDelete;
final Function(MembreModel)? onEdit;
final Function(MembreModel)? onDelete;
final bool isAlternate;
const MembreRowWidget({
Key? key,
super.key,
required this.membre,
this.onEdit,
this.onDelete,
}) : super(key: key);
this.isAlternate = false,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
// Couleur de fond alternée
final backgroundColor = isAlternate ? theme.colorScheme.surface : theme.colorScheme.surface;
return InkWell(
onTap: () => _showMembreDetails(context),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
decoration: BoxDecoration(
color: backgroundColor,
),
child: Row(
children: [
// ID
Expanded(
flex: 1,
child: Text(
membre.id.toString(),
style: theme.textTheme.bodyMedium,
),
),
// Prénom (firstName)
Expanded(
flex: 2,
child: Text(
membre.firstName,
style: theme.textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
),
// Nom (name)
Expanded(
flex: 2,
child: Text(
membre.name,
style: theme.textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
),
// Email
Expanded(
flex: 3,
child: Text(
membre.email,
style: theme.textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
),
// Rôle (fkRole)
Expanded(
flex: 1,
child: Text(
_getRoleName(membre.fkRole),
style: theme.textTheme.bodyMedium,
),
),
// Statut (actif/inactif)
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),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: membre.chkActive == 1 ? Colors.green.withOpacity(0.3) : Colors.red.withOpacity(0.3),
),
),
child: Text(
membre.chkActive == 1 ? 'Actif' : 'Inactif',
style: theme.textTheme.bodySmall?.copyWith(
color: membre.chkActive == 1 ? Colors.green[700] : Colors.red[700],
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
),
// Actions
if (onEdit != null || onDelete != null)
Expanded(
flex: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Bouton Edit
if (onEdit != null)
IconButton(
icon: Icon(
Icons.edit,
size: 20,
color: theme.colorScheme.primary,
),
onPressed: () => onEdit!(membre),
tooltip: 'Modifier',
constraints: const BoxConstraints(
minWidth: 36,
minHeight: 36,
),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
),
// Espacement entre les boutons
if (onEdit != null && onDelete != null) const SizedBox(width: 8),
// Bouton Delete
if (onDelete != null)
IconButton(
icon: Icon(
Icons.delete,
size: 20,
color: theme.colorScheme.error,
),
onPressed: () => onDelete!(membre),
tooltip: 'Supprimer',
constraints: const BoxConstraints(
minWidth: 36,
minHeight: 36,
),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
),
],
),
),
],
),
),
);
}
// Afficher les détails du membre dans une boîte de dialogue
void _showMembreDetails(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
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('Secteur', membre.sectName ?? 'Non défini'),
_buildDetailRow('Statut', membre.chkActive == 1 ? 'Actif' : 'Inactif'),
if (membre.dateNaissance != null)
_buildDetailRow('Date de naissance', '${membre.dateNaissance!.day}/${membre.dateNaissance!.month}/${membre.dateNaissance!.year}'),
if (membre.dateEmbauche != null)
_buildDetailRow('Date d\'embauche', '${membre.dateEmbauche!.day}/${membre.dateEmbauche!.month}/${membre.dateEmbauche!.year}'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
],
),
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ID
Expanded(
flex: 1,
SizedBox(
width: 120,
child: Text(
membre.id.toString(),
style: theme.textTheme.bodyMedium,
'$label:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
// Prénom (firstName)
Expanded(
flex: 2,
child: Text(
membre.firstName,
style: theme.textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
),
// Nom (name)
Expanded(
flex: 2,
child: Text(
membre.name,
style: theme.textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
),
// Secteur (sectName)
Expanded(
flex: 2,
child: Text(
membre.sectName ?? '',
style: theme.textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
),
),
// Rôle (fkRole)
Expanded(
flex: 1,
child: Text(
_getRoleName(membre.fkRole),
style: theme.textTheme.bodyMedium,
),
),
// Actions
Expanded(
flex: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Bouton Edit
IconButton(
icon: const Icon(Icons.edit, size: 20),
color: theme.colorScheme.primary,
onPressed: onEdit,
tooltip: 'Modifier',
constraints: const BoxConstraints(),
padding: const EdgeInsets.all(8),
),
// Bouton Delete
IconButton(
icon: const Icon(Icons.delete, size: 20),
color: theme.colorScheme.error,
onPressed: onDelete,
tooltip: 'Supprimer',
constraints: const BoxConstraints(),
padding: const EdgeInsets.all(8),
),
],
),
child: Text(value),
),
],
),

View File

@@ -1,24 +1,31 @@
import 'package:flutter/material.dart';
import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/core/repositories/membre_repository.dart';
import 'package:geosector_app/presentation/widgets/membre_row_widget.dart';
class MembreTableWidget extends StatelessWidget {
final List<MembreModel> membres;
final Function(MembreModel)? onEdit;
final Function(MembreModel)? onDelete;
final MembreRepository membreRepository;
final bool showHeader;
final double? height;
final EdgeInsetsGeometry? padding;
final bool isLoading;
final String? emptyMessage;
const MembreTableWidget({
Key? key,
super.key,
required this.membres,
required this.membreRepository,
this.onEdit,
this.onDelete,
this.showHeader = true,
this.height,
this.padding,
}) : super(key: key);
this.isLoading = false,
this.emptyMessage,
});
@override
Widget build(BuildContext context) {
@@ -44,8 +51,7 @@ class MembreTableWidget extends StatelessWidget {
// En-tête du tableau
if (showHeader)
Padding(
padding:
const EdgeInsets.only(bottom: 16.0, left: 16.0, right: 16.0),
padding: const EdgeInsets.only(bottom: 16.0, left: 16.0, right: 16.0),
child: Row(
children: [
// ID
@@ -55,6 +61,7 @@ class MembreTableWidget extends StatelessWidget {
'ID',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
),
@@ -66,6 +73,7 @@ class MembreTableWidget extends StatelessWidget {
'Prénom',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
),
@@ -77,17 +85,19 @@ class MembreTableWidget extends StatelessWidget {
'Nom',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
),
// Secteur (sectName)
// Email
Expanded(
flex: 2,
flex: 3,
child: Text(
'Secteur',
'Email',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
),
@@ -99,51 +109,83 @@ class MembreTableWidget extends StatelessWidget {
'Rôle',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
),
// Actions
// Statut
Expanded(
flex: 2,
flex: 1,
child: Text(
'Actions',
'Statut',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
textAlign: TextAlign.end,
),
),
// Actions (si onEdit ou onDelete sont fournis)
if (onEdit != null || onDelete != null)
Expanded(
flex: 2,
child: Text(
'Actions',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
textAlign: TextAlign.end,
),
),
],
),
),
// Liste des membres
// Corps du tableau
Expanded(
child: membres.isEmpty
? Center(
child: Text(
'Aucun membre disponible',
style: theme.textTheme.bodyMedium,
),
)
: ListView.separated(
itemCount: membres.length,
separatorBuilder: (context, index) =>
const SizedBox(height: 8.0),
itemBuilder: (context, index) {
final membre = membres[index];
return MembreRowWidget(
membre: membre,
onEdit: onEdit != null ? () => onEdit!(membre) : null,
onDelete:
onDelete != null ? () => onDelete!(membre) : null,
);
},
),
child: _buildTableContent(context),
),
],
),
);
}
Widget _buildTableContent(BuildContext context) {
// Afficher un indicateur de chargement si isLoading est true
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
// Afficher un message si la liste est vide
if (membres.isEmpty) {
return Center(
child: Text(
emptyMessage ?? 'Aucun membre trouvé',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
);
}
// Afficher la liste des membres
return ListView.separated(
itemCount: membres.length,
separatorBuilder: (context, index) => Divider(
color: Theme.of(context).dividerColor.withOpacity(0.3),
height: 1,
),
itemBuilder: (context, index) {
final membre = membres[index];
return MembreRowWidget(
membre: membre,
onEdit: onEdit,
onDelete: onDelete,
isAlternate: index % 2 == 1,
);
},
);
}
}