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

@@ -1,26 +0,0 @@
# Structure de présentation
Ce dossier contient tous les éléments liés à l'interface utilisateur de l'application, organisés comme suit :
## Sous-dossiers
- `/admin` : Pages et widgets spécifiques à l'interface administrateur
- `/user` : Pages et widgets spécifiques à l'interface utilisateur
- `/auth` : Pages et widgets liés à l'authentification
- `/public` : Pages et widgets accessibles sans authentification
- `/widgets` : Widgets partagés utilisés dans plusieurs parties de l'application
## Organisation des fichiers
Chaque sous-dossier peut contenir :
- Des pages (écrans complets)
- Des widgets spécifiques à cette section
- Des modèles de données d'UI
- Des utilitaires d'UI spécifiques
## Bonnes pratiques
- Les widgets réutilisables dans plusieurs sections doivent être placés dans `/widgets`
- Les widgets spécifiques à une section doivent être placés dans le sous-dossier correspondant
- Utiliser des imports relatifs pour les fichiers du même module
- Utiliser des imports absolus pour les fichiers d'autres modules

View File

@@ -0,0 +1,370 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/core/repositories/amicale_repository.dart';
import 'package:geosector_app/core/repositories/membre_repository.dart';
import 'package:geosector_app/presentation/widgets/amicale_table_widget.dart';
import 'package:geosector_app/presentation/widgets/membre_table_widget.dart';
/// Class pour dessiner les petits points blancs sur le fond
class DotsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.5)
..style = PaintingStyle.fill;
final random = math.Random(42); // Seed fixe pour consistance
final numberOfDots = (size.width * size.height) ~/ 1500;
for (int i = 0; i < numberOfDots; i++) {
final x = random.nextDouble() * size.width;
final y = random.nextDouble() * size.height;
final radius = 1.0 + random.nextDouble() * 2.0;
canvas.drawCircle(Offset(x, y), radius, paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
/// Page d'administration de l'amicale et des membres
/// Cette page est intégrée dans le tableau de bord administrateur
class AdminAmicalePage extends StatefulWidget {
final UserRepository userRepository;
final AmicaleRepository amicaleRepository;
final MembreRepository membreRepository;
const AdminAmicalePage({
super.key,
required this.userRepository,
required this.amicaleRepository,
required this.membreRepository,
});
@override
State<AdminAmicalePage> createState() => _AdminAmicalePageState();
}
class _AdminAmicalePageState extends State<AdminAmicalePage> {
UserModel? _currentUser;
String? _errorMessage;
@override
void initState() {
super.initState();
_loadCurrentUser();
}
void _loadCurrentUser() {
final currentUser = widget.userRepository.getCurrentUser();
if (currentUser == null) {
setState(() {
_errorMessage = 'Utilisateur non connecté';
});
return;
}
if (currentUser.fkEntite == null) {
setState(() {
_errorMessage = 'Utilisateur non associé à une amicale';
});
return;
}
setState(() {
_currentUser = currentUser;
_errorMessage = null;
});
}
void _handleEditAmicale(AmicaleModel amicale) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Modifier l\'amicale'),
content: Text('Voulez-vous modifier l\'amicale ${amicale.name} ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
// TODO: Naviguer vers la page de modification
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) => EditAmicalePage(
// amicale: amicale,
// amicaleRepository: widget.amicaleRepository,
// ),
// ),
// );
},
child: const Text('Modifier'),
),
],
),
);
}
void _handleEditMembre(MembreModel membre) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Modifier le membre'),
content: Text('Voulez-vous modifier le membre ${membre.firstName} ${membre.name} ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
// TODO: Naviguer vers la page de modification
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) => EditMembrePage(
// membre: membre,
// membreRepository: widget.membreRepository,
// ),
// ),
// );
},
child: const Text('Modifier'),
),
],
),
);
}
void _handleAddMembre() {
if (_currentUser?.fkEntite == null) return;
// TODO: Naviguer vers la page d'ajout de membre
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) => AddMembrePage(
// amicaleId: _currentUser!.fkEntite!,
// membreRepository: widget.membreRepository,
// ),
// ),
// );
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Stack(
children: [
// Fond dégradé avec petits points blancs
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.white, Colors.red.shade300],
),
),
child: CustomPaint(
painter: DotsPainter(),
child: const SizedBox(width: double.infinity, height: double.infinity),
),
),
// Contenu principal
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre de la page
Text(
'Mon amicale et ses membres',
style: theme.textTheme.headlineMedium?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
// Message d'erreur si présent
if (_errorMessage != null)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.withOpacity(0.3)),
),
child: Row(
children: [
const Icon(Icons.error_outline, color: Colors.red),
const SizedBox(width: 12),
Expanded(
child: Text(
_errorMessage!,
style: const TextStyle(color: Colors.red),
),
),
],
),
),
// Contenu principal avec ValueListenableBuilder
if (_currentUser != null && _currentUser!.fkEntite != null)
Expanded(
child: ValueListenableBuilder<Box<AmicaleModel>>(
valueListenable: widget.amicaleRepository.getAmicalesBox().listenable(),
builder: (context, amicalesBox, child) {
final amicale = amicalesBox.get(_currentUser!.fkEntite!);
if (amicale == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.business_outlined,
size: 64,
color: theme.colorScheme.primary.withOpacity(0.7),
),
const SizedBox(height: 16),
Text(
'Amicale non trouvée',
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'L\'amicale associée à votre compte n\'existe plus.',
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge,
),
],
),
);
}
return ValueListenableBuilder<Box<MembreModel>>(
valueListenable: widget.membreRepository.getMembresBox().listenable(),
builder: (context, membresBox, child) {
// Filtrer les membres par amicale
// Note: Il faudra ajouter le champ fkEntite au modèle MembreModel
final membres = membresBox.values.where((membre) => membre.fkEntite == _currentUser!.fkEntite).toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section Amicale
Text(
'Informations de l\'amicale',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
// Tableau Amicale
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: AmicaleTableWidget(
amicales: [amicale],
onEdit: null,
onDelete: null,
amicaleRepository: widget.amicaleRepository,
userRepository: widget.userRepository,
apiService: null, // Ou passez l'ApiService si vous l'avez disponible
showActionsColumn: false,
),
),
const SizedBox(height: 32),
// Section Membres
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Membres de l\'amicale (${membres.length})',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
ElevatedButton.icon(
onPressed: _handleAddMembre,
icon: const Icon(Icons.add),
label: const Text('Ajouter un membre'),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: Colors.white,
),
),
],
),
const SizedBox(height: 16),
// Tableau Membres
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: MembreTableWidget(
membres: membres,
onEdit: _handleEditMembre,
onDelete: null, // Géré par l'admin principal
membreRepository: widget.membreRepository,
),
),
),
],
);
},
);
},
),
),
// Message si pas d'utilisateur connecté
if (_currentUser == null)
const Expanded(
child: Center(
child: CircularProgressIndicator(),
),
),
],
),
),
],
);
}
}

View File

@@ -11,7 +11,7 @@ import 'admin_statistics_page.dart';
import 'admin_history_page.dart';
import 'admin_communication_page.dart';
import 'admin_map_page.dart';
import 'admin_entite.dart';
import 'admin_amicale_page.dart';
/// Class pour dessiner les petits points blancs sur le fond
class DotsPainter extends CustomPainter {
@@ -37,14 +37,13 @@ class DotsPainter extends CustomPainter {
}
class AdminDashboardPage extends StatefulWidget {
const AdminDashboardPage({Key? key}) : super(key: key);
const AdminDashboardPage({super.key});
@override
State<AdminDashboardPage> createState() => _AdminDashboardPageState();
}
class _AdminDashboardPageState extends State<AdminDashboardPage>
with WidgetsBindingObserver {
class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBindingObserver {
int _selectedIndex = 0;
// Liste des pages à afficher
@@ -59,31 +58,31 @@ class _AdminDashboardPageState extends State<AdminDashboardPage>
label: 'Tableau de bord',
icon: Icons.dashboard_outlined,
selectedIcon: Icons.dashboard,
page: AdminDashboardHomePage(),
pageType: _PageType.dashboardHome,
),
const _NavigationItem(
label: 'Statistiques',
icon: Icons.bar_chart_outlined,
selectedIcon: Icons.bar_chart,
page: AdminStatisticsPage(),
pageType: _PageType.statistics,
),
const _NavigationItem(
label: 'Historique',
icon: Icons.history_outlined,
selectedIcon: Icons.history,
page: AdminHistoryPage(),
pageType: _PageType.history,
),
const _NavigationItem(
label: 'Messages',
icon: Icons.chat_outlined,
selectedIcon: Icons.chat,
page: AdminCommunicationPage(),
pageType: _PageType.communication,
),
const _NavigationItem(
label: 'Carte',
icon: Icons.map_outlined,
selectedIcon: Icons.map,
page: AdminMapPage(),
pageType: _PageType.map,
),
];
@@ -93,18 +92,42 @@ class _AdminDashboardPageState extends State<AdminDashboardPage>
label: 'Amicale & membres',
icon: Icons.business_outlined,
selectedIcon: Icons.business,
page: AdminEntitePage(),
pageType: _PageType.amicale,
requiredRole: 2,
),
const _NavigationItem(
label: 'Opérations',
icon: Icons.calendar_today_outlined,
selectedIcon: Icons.calendar_today,
page: Scaffold(body: Center(child: Text('Page Opérations'))),
pageType: _PageType.operations,
requiredRole: 2,
),
];
// Construire la page basée sur le type
Widget _buildPage(_PageType pageType) {
switch (pageType) {
case _PageType.dashboardHome:
return const AdminDashboardHomePage();
case _PageType.statistics:
return const AdminStatisticsPage();
case _PageType.history:
return const AdminHistoryPage();
case _PageType.communication:
return const AdminCommunicationPage();
case _PageType.map:
return const AdminMapPage();
case _PageType.amicale:
return AdminAmicalePage(
userRepository: userRepository,
amicaleRepository: amicaleRepository,
membreRepository: membreRepository,
);
case _PageType.operations:
return const Scaffold(body: Center(child: Text('Page Opérations')));
}
}
// Construire la liste des destinations de navigation en fonction du rôle
List<NavigationDestination> _buildNavigationDestinations() {
final destinations = <NavigationDestination>[];
@@ -145,13 +168,15 @@ class _AdminDashboardPageState extends State<AdminDashboardPage>
final currentUser = userRepository.getCurrentUser();
// Ajouter les pages de base
pages.addAll(_baseNavigationItems.map((item) => item.page));
for (final item in _baseNavigationItems) {
pages.add(_buildPage(item.pageType));
}
// Ajouter les pages admin si l'utilisateur a le rôle requis
if (currentUser?.role == 2) {
for (final item in _adminNavigationItems) {
if (item.requiredRole == null || item.requiredRole == 2) {
pages.add(item.page);
pages.add(_buildPage(item.pageType));
}
}
}
@@ -171,11 +196,9 @@ class _AdminDashboardPageState extends State<AdminDashboardPage>
debugPrint('userRepository est correctement initialisé');
final currentUser = userRepository.getCurrentUser();
if (currentUser == null) {
debugPrint(
'ERREUR: Aucun utilisateur connecté dans AdminDashboardPage');
debugPrint('ERREUR: Aucun utilisateur connecté dans AdminDashboardPage');
} else {
debugPrint(
'Utilisateur connecté: ${currentUser.username} (${currentUser.id})');
debugPrint('Utilisateur connecté: ${currentUser.username} (${currentUser.id})');
}
userRepository.addListener(_handleUserRepositoryChanges);
@@ -276,7 +299,7 @@ class _AdminDashboardPageState extends State<AdminDashboardPage>
),
child: CustomPaint(
painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity),
child: const SizedBox(width: double.infinity, height: double.infinity),
),
),
// Contenu de la page
@@ -299,19 +322,30 @@ class _AdminDashboardPageState extends State<AdminDashboardPage>
}
}
// Enum pour les types de pages
enum _PageType {
dashboardHome,
statistics,
history,
communication,
map,
amicale,
operations,
}
// Classe pour représenter une destination de navigation avec sa page associée
class _NavigationItem {
final String label;
final IconData icon;
final IconData selectedIcon;
final Widget page;
final _PageType pageType;
final int? requiredRole; // null si accessible à tous les rôles
const _NavigationItem({
required this.label,
required this.icon,
required this.selectedIcon,
required this.page,
required this.pageType,
this.requiredRole,
});
}

View File

@@ -1,361 +0,0 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/presentation/widgets/amicale_table_widget.dart';
import 'package:geosector_app/presentation/widgets/membre_table_widget.dart';
/// Class pour dessiner les petits points blancs sur le fond
class DotsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.5)
..style = PaintingStyle.fill;
final random = math.Random(42); // Seed fixe pour consistance
final numberOfDots = (size.width * size.height) ~/ 1500;
for (int i = 0; i < numberOfDots; i++) {
final x = random.nextDouble() * size.width;
final y = random.nextDouble() * size.height;
final radius = 1.0 + random.nextDouble() * 2.0;
canvas.drawCircle(Offset(x, y), radius, paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
/// Page d'administration de l'amicale et des membres
/// Cette page est intégrée dans le tableau de bord administrateur
class AdminEntitePage extends StatefulWidget {
const AdminEntitePage({Key? key}) : super(key: key);
@override
State<AdminEntitePage> createState() => _AdminEntitePageState();
}
class _AdminEntitePageState extends State<AdminEntitePage> {
bool _isLoading = true;
AmicaleModel? _amicale;
List<MembreModel> _membres = [];
String? _errorMessage;
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
// Récupérer l'utilisateur connecté en utilisant l'instance globale
final currentUser = userRepository.getCurrentUser();
if (currentUser == null) {
setState(() {
_errorMessage = 'Utilisateur non connecté';
_isLoading = false;
});
return;
}
// Vérifier si fkEntite est null
if (currentUser.fkEntite == null) {
setState(() {
_errorMessage = 'Utilisateur non associé à une amicale';
_isLoading = false;
});
return;
}
// Récupérer l'amicale de l'utilisateur en utilisant l'instance globale
final amicale = amicaleRepository.getAmicaleById(currentUser.fkEntite!);
if (amicale == null) {
setState(() {
_errorMessage = 'Amicale non trouvée';
_isLoading = false;
});
return;
}
// Récupérer tous les membres
// Note: Dans un cas réel, nous devrions filtrer les membres par amicale,
// mais le modèle MembreModel n'a pas de champ fkEntite pour le moment
final membres = membreRepository.getAllMembres();
setState(() {
_amicale = amicale;
_membres = membres;
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = 'Erreur lors du chargement des données: $e';
_isLoading = false;
});
}
}
void _handleEditAmicale(AmicaleModel amicale) {
// Afficher une boîte de dialogue de confirmation
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Modifier l\'amicale'),
content: Text('Voulez-vous modifier l\'amicale ${amicale.name} ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
// Naviguer vers la page de modification
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) => EditAmicalePage(amicale: amicale),
// ),
// );
},
child: const Text('Modifier'),
),
],
),
);
}
void _handleEditMembre(MembreModel membre) {
// Afficher une boîte de dialogue de confirmation
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Modifier le membre'),
content: Text(
'Voulez-vous modifier le membre ${membre.firstName} ${membre.name} ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
// Naviguer vers la page de modification
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) => EditMembrePage(membre: membre),
// ),
// );
},
child: const Text('Modifier'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Stack(
children: [
// Fond dégradé avec petits points blancs
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.white, Colors.red.shade300],
),
),
child: CustomPaint(
painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity),
),
),
// Contenu principal
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre de la page
Text(
'Mon amicale et ses membres',
style: theme.textTheme.headlineMedium?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
// Message d'erreur si présent
if (_errorMessage != null)
Container(
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.withOpacity(0.3)),
),
child: Row(
children: [
const Icon(Icons.error_outline, color: Colors.red),
const SizedBox(width: 12),
Expanded(
child: Text(
_errorMessage!,
style: const TextStyle(color: Colors.red),
),
),
],
),
),
// Contenu principal
if (_isLoading)
const Expanded(
child: Center(
child: CircularProgressIndicator(),
),
)
else if (_amicale == null)
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.business_outlined,
size: 64,
color: theme.colorScheme.primary.withOpacity(0.7),
),
const SizedBox(height: 16),
Text(
'Aucune amicale associée',
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'Vous n\'êtes pas associé à une amicale.',
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge,
),
],
),
),
)
else
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section Amicale
Text(
'Informations de l\'amicale',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
// Tableau Amicale
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: AmicaleTableWidget(
amicales: [_amicale!],
// Pas de bouton de suppression pour sa propre amicale
onDelete: null,
),
),
const SizedBox(height: 32),
// Section Membres
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Membres de l\'amicale',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
ElevatedButton.icon(
onPressed: () {
// Naviguer vers la page d'ajout de membre
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) => AddMembrePage(amicaleId: _amicale!.id),
// ),
// );
},
icon: const Icon(Icons.add),
label: const Text('Ajouter un membre'),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: Colors.white,
),
),
],
),
const SizedBox(height: 16),
// Tableau Membres
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: MembreTableWidget(
membres: _membres,
onEdit: _handleEditMembre,
// Pas de bouton de suppression pour les membres de sa propre amicale
// sauf si l'utilisateur a un rôle élevé
onDelete: null,
),
),
),
],
),
),
],
),
),
],
);
}
}

View File

@@ -78,9 +78,7 @@ class _LoginPageState extends State<LoginPage> {
// Fallback sur la version du AppInfoService si elle existe
if (mounted) {
setState(() {
_appVersion = AppInfoService.fullVersion
.split(' ')
.last; // Extraire juste le numéro
_appVersion = AppInfoService.fullVersion.split(' ').last; // Extraire juste le numéro
});
}
}
@@ -103,8 +101,7 @@ class _LoginPageState extends State<LoginPage> {
// Vérification du type de connexion
if (widget.loginType == null) {
// Si aucun type n'est spécifié, naviguer vers la splash page
print(
'LoginPage: Aucun type de connexion spécifié, navigation vers splash page');
print('LoginPage: Aucun type de connexion spécifié, navigation vers splash page');
WidgetsBinding.instance.addPostFrameCallback((_) {
GoRouter.of(context).go('/');
});
@@ -157,13 +154,10 @@ class _LoginPageState extends State<LoginPage> {
'''
]);
if (result != null &&
result is String &&
result.toLowerCase() == 'user') {
if (result != null && result is String && result.toLowerCase() == 'user') {
setState(() {
_loginType = 'user';
print(
'LoginPage: Type détecté depuis sessionStorage: $_loginType');
print('LoginPage: Type détecté depuis sessionStorage: $_loginType');
});
}
} catch (e) {
@@ -217,7 +211,7 @@ class _LoginPageState extends State<LoginPage> {
if (lastUser.role is String) {
roleValue = int.tryParse(lastUser.role as String) ?? 0;
} else {
roleValue = lastUser.role as int;
roleValue = lastUser.role;
}
// Vérifier si le rôle correspond au type de login
@@ -227,8 +221,7 @@ class _LoginPageState extends State<LoginPage> {
debugPrint('Rôle utilisateur (1) correspond au type de login (user)');
} else if (_loginType == 'admin' && roleValue > 1) {
roleMatches = true;
debugPrint(
'Rôle administrateur (${roleValue}) correspond au type de login (admin)');
debugPrint('Rôle administrateur ($roleValue) correspond au type de login (admin)');
}
// Pré-remplir le champ username seulement si le rôle correspond
@@ -242,12 +235,10 @@ class _LoginPageState extends State<LoginPage> {
} else if (lastUser.email.isNotEmpty) {
_usernameController.text = lastUser.email;
_usernameFocusNode.unfocus();
debugPrint(
'Champ username pré-rempli avec email: ${lastUser.email}');
debugPrint('Champ username pré-rempli avec email: ${lastUser.email}');
}
} else {
debugPrint(
'Le rôle (${roleValue}) ne correspond pas au type de login ($_loginType), champ username non pré-rempli');
debugPrint('Le rôle ($roleValue) ne correspond pas au type de login ($_loginType), champ username non pré-rempli');
}
}
});
@@ -327,14 +318,12 @@ class _LoginPageState extends State<LoginPage> {
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: _loginType == 'user'
? [Colors.white, Colors.green.shade300]
: [Colors.white, Colors.red.shade300],
colors: _loginType == 'user' ? [Colors.white, Colors.green.shade300] : [Colors.white, Colors.red.shade300],
),
),
child: CustomPaint(
painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity),
child: const SizedBox(width: double.infinity, height: double.infinity),
),
),
SafeArea(
@@ -345,11 +334,8 @@ class _LoginPageState extends State<LoginPage> {
constraints: const BoxConstraints(maxWidth: 500),
child: Card(
elevation: 8,
shadowColor: _loginType == 'user'
? Colors.green.withOpacity(0.5)
: Colors.red.withOpacity(0.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0)),
shadowColor: _loginType == 'user' ? Colors.green.withOpacity(0.5) : Colors.red.withOpacity(0.5),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
@@ -378,9 +364,7 @@ class _LoginPageState extends State<LoginPage> {
decoration: BoxDecoration(
color: theme.colorScheme.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color:
theme.colorScheme.error.withOpacity(0.3)),
border: Border.all(color: theme.colorScheme.error.withOpacity(0.3)),
),
child: Column(
children: [
@@ -391,8 +375,7 @@ class _LoginPageState extends State<LoginPage> {
),
const SizedBox(height: 16),
Text(
_locationErrorMessage ??
'L\'accès à la localisation est nécessaire pour utiliser cette application.',
_locationErrorMessage ?? 'L\'accès à la localisation est nécessaire pour utiliser cette application.',
style: theme.textTheme.bodyLarge,
textAlign: TextAlign.center,
),
@@ -416,14 +399,10 @@ class _LoginPageState extends State<LoginPage> {
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
_buildInstructionStep(theme, 1,
'Ouvrez les paramètres de votre appareil'),
_buildInstructionStep(theme, 2,
'Accédez aux paramètres de confidentialité ou de localisation'),
_buildInstructionStep(theme, 3,
'Recherchez GEOSECTOR dans la liste des applications'),
_buildInstructionStep(theme, 4,
'Activez l\'accès à la localisation pour cette application'),
_buildInstructionStep(theme, 1, 'Ouvrez les paramètres de votre appareil'),
_buildInstructionStep(theme, 2, 'Accédez aux paramètres de confidentialité ou de localisation'),
_buildInstructionStep(theme, 3, 'Recherchez GEOSECTOR dans la liste des applications'),
_buildInstructionStep(theme, 4, 'Activez l\'accès à la localisation pour cette application'),
const SizedBox(height: 32),
// Boutons d'action
@@ -469,8 +448,7 @@ class _LoginPageState extends State<LoginPage> {
}
/// Construit une étape d'instruction pour activer la localisation
Widget _buildInstructionStep(
ThemeData theme, int stepNumber, String instruction) {
Widget _buildInstructionStep(ThemeData theme, int stepNumber, String instruction) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
@@ -530,14 +508,12 @@ class _LoginPageState extends State<LoginPage> {
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: _loginType == 'user'
? [Colors.white, Colors.green.shade300]
: [Colors.white, Colors.red.shade300],
colors: _loginType == 'user' ? [Colors.white, Colors.green.shade300] : [Colors.white, Colors.red.shade300],
),
),
child: CustomPaint(
painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity),
child: const SizedBox(width: double.infinity, height: double.infinity),
),
),
SafeArea(
@@ -548,11 +524,8 @@ class _LoginPageState extends State<LoginPage> {
constraints: const BoxConstraints(maxWidth: 500),
child: Card(
elevation: 8,
shadowColor: _loginType == 'user'
? Colors.green.withOpacity(0.5)
: Colors.red.withOpacity(0.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0)),
shadowColor: _loginType == 'user' ? Colors.green.withOpacity(0.5) : Colors.red.withOpacity(0.5),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.0)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
@@ -566,39 +539,26 @@ class _LoginPageState extends State<LoginPage> {
),
const SizedBox(height: 24),
Text(
_loginType == 'user'
? 'Connexion Utilisateur'
: 'Connexion Administrateur',
_loginType == 'user' ? 'Connexion Utilisateur' : 'Connexion Administrateur',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: _loginType == 'user'
? Colors.green
: Colors.red,
color: _loginType == 'user' ? Colors.green : Colors.red,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
AppInfoService.fullVersion,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.primary.withOpacity(0.7),
),
),
const SizedBox(height: 8),
// Ajouter un texte de débogage uniquement en mode développement
if (kDebugMode)
Text(
'Type de connexion: $_loginType',
style:
TextStyle(fontSize: 10, color: Colors.grey),
style: const TextStyle(fontSize: 10, color: Colors.grey),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Bienvenue sur GEOSECTOR',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onBackground
.withOpacity(0.7),
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
@@ -616,23 +576,17 @@ class _LoginPageState extends State<LoginPage> {
color: theme.colorScheme.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color:
theme.colorScheme.error.withOpacity(0.3),
color: theme.colorScheme.error.withOpacity(0.3),
),
),
child: Column(
children: [
Icon(Icons.signal_wifi_off,
color: theme.colorScheme.error, size: 32),
Icon(Icons.signal_wifi_off, color: theme.colorScheme.error, size: 32),
const SizedBox(height: 8),
Text('Connexion Internet requise',
style: theme.textTheme.titleMedium
?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.error)),
style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold, color: theme.colorScheme.error)),
const SizedBox(height: 8),
const Text(
'Veuillez vous connecter à Internet (WiFi ou données mobiles) pour pouvoir vous connecter.'),
const Text('Veuillez vous connecter à Internet (WiFi ou données mobiles) pour pouvoir vous connecter.'),
],
),
),
@@ -669,9 +623,7 @@ class _LoginPageState extends State<LoginPage> {
obscureText: _obscurePassword,
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
_obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
),
onPressed: () {
setState(() {
@@ -686,21 +638,17 @@ class _LoginPageState extends State<LoginPage> {
return null;
},
onFieldSubmitted: (_) async {
if (!userRepository.isLoading &&
_formKey.currentState!.validate()) {
if (!userRepository.isLoading && _formKey.currentState!.validate()) {
// Vérifier que le type de connexion est spécifié
if (_loginType.isEmpty) {
print(
'Login: Type non spécifié, redirection vers la page de démarrage');
print('Login: Type non spécifié, redirection vers la page de démarrage');
context.go('/');
return;
}
print(
'Login: Tentative avec type: $_loginType');
print('Login: Tentative avec type: $_loginType');
final success =
await userRepository.login(
final success = await userRepository.login(
_usernameController.text.trim(),
_passwordController.text,
type: _loginType,
@@ -708,16 +656,12 @@ class _LoginPageState extends State<LoginPage> {
if (success && mounted) {
// Récupérer directement le rôle de l'utilisateur
final user =
userRepository.getCurrentUser();
final user = userRepository.getCurrentUser();
if (user == null) {
debugPrint(
'ERREUR: Utilisateur non trouvé après connexion réussie');
ScaffoldMessenger.of(context)
.showSnackBar(
debugPrint('ERREUR: Utilisateur non trouvé après connexion réussie');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Erreur de connexion. Veuillez réessayer.'),
content: Text('Erreur de connexion. Veuillez réessayer.'),
backgroundColor: Colors.red,
),
);
@@ -727,32 +671,25 @@ class _LoginPageState extends State<LoginPage> {
// Convertir le rôle en int si nécessaire
int roleValue;
if (user.role is String) {
roleValue = int.tryParse(
user.role as String) ??
1;
roleValue = int.tryParse(user.role as String) ?? 1;
} else {
roleValue = user.role as int;
roleValue = user.role;
}
debugPrint(
'Role de l\'utilisateur: $roleValue');
debugPrint('Role de l\'utilisateur: $roleValue');
// Redirection simple basée sur le rôle
if (roleValue > 1) {
debugPrint(
'Redirection vers /admin (rôle > 1)');
debugPrint('Redirection vers /admin (rôle > 1)');
context.go('/admin');
} else {
debugPrint(
'Redirection vers /user (rôle = 1)');
debugPrint('Redirection vers /user (rôle = 1)');
context.go('/user');
}
} else if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Échec de la connexion. Vérifiez vos identifiants.'),
content: Text('Échec de la connexion. Vérifiez vos identifiants.'),
backgroundColor: Colors.red,
),
);
@@ -781,23 +718,19 @@ class _LoginPageState extends State<LoginPage> {
// Bouton de connexion
CustomButton(
onPressed: (userRepository.isLoading ||
!_isConnected)
onPressed: (userRepository.isLoading || !_isConnected)
? null
: () async {
if (_formKey.currentState!
.validate()) {
if (_formKey.currentState!.validate()) {
// Vérifier à nouveau les permissions de géolocalisation avant de se connecter (sauf en version web)
if (!kIsWeb) {
await _checkLocationPermission();
// Si l'utilisateur n'a toujours pas accordé les permissions, ne pas continuer
if (!_hasLocationPermission) {
ScaffoldMessenger.of(context)
.showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'L\'accès à la localisation est nécessaire pour utiliser cette application.'),
content: Text('L\'accès à la localisation est nécessaire pour utiliser cette application.'),
backgroundColor: Colors.red,
),
);
@@ -806,36 +739,23 @@ class _LoginPageState extends State<LoginPage> {
}
// Vérifier la connexion Internet
await connectivityService
.checkConnectivity();
await connectivityService.checkConnectivity();
if (!connectivityService
.isConnected) {
ScaffoldMessenger.of(context)
.showSnackBar(
if (!connectivityService.isConnected) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text(
'Aucune connexion Internet. La connexion n\'est pas possible hors ligne.'),
backgroundColor:
theme.colorScheme.error,
duration: const Duration(
seconds: 3),
content: const Text('Aucune connexion Internet. La connexion n\'est pas possible hors ligne.'),
backgroundColor: theme.colorScheme.error,
duration: const Duration(seconds: 3),
action: SnackBarAction(
label: 'Réessayer',
onPressed: () async {
await connectivityService
.checkConnectivity();
if (connectivityService
.isConnected &&
mounted) {
ScaffoldMessenger.of(
context)
.showSnackBar(
await connectivityService.checkConnectivity();
if (connectivityService.isConnected && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Connexion Internet ${connectivityService.connectionType} détectée.'),
backgroundColor:
Colors.green,
content: Text('Connexion Internet ${connectivityService.connectionType} détectée.'),
backgroundColor: Colors.green,
),
);
}
@@ -848,18 +768,15 @@ class _LoginPageState extends State<LoginPage> {
// Vérifier que le type de connexion est spécifié
if (_loginType.isEmpty) {
print(
'Login: Type non spécifié, redirection vers la page de démarrage');
print('Login: Type non spécifié, redirection vers la page de démarrage');
context.go('/');
return;
}
print(
'Login: Tentative avec type: $_loginType');
print('Login: Tentative avec type: $_loginType');
// Utiliser directement userRepository avec l'overlay de chargement
final success = await userRepository
.loginWithUI(
final success = await userRepository.loginWithUI(
context,
_usernameController.text.trim(),
_passwordController.text,
@@ -867,20 +784,15 @@ class _LoginPageState extends State<LoginPage> {
);
if (success && mounted) {
debugPrint(
'Connexion réussie, tentative de redirection...');
debugPrint('Connexion réussie, tentative de redirection...');
// Récupérer directement le rôle de l'utilisateur
final user = userRepository
.getCurrentUser();
final user = userRepository.getCurrentUser();
if (user == null) {
debugPrint(
'ERREUR: Utilisateur non trouvé après connexion réussie');
ScaffoldMessenger.of(context)
.showSnackBar(
debugPrint('ERREUR: Utilisateur non trouvé après connexion réussie');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Erreur de connexion. Veuillez réessayer.'),
content: Text('Erreur de connexion. Veuillez réessayer.'),
backgroundColor: Colors.red,
),
);
@@ -890,41 +802,32 @@ class _LoginPageState extends State<LoginPage> {
// Convertir le rôle en int si nécessaire
int roleValue;
if (user.role is String) {
roleValue = int.tryParse(
user.role as String) ??
1;
roleValue = int.tryParse(user.role as String) ?? 1;
} else {
roleValue = user.role as int;
roleValue = user.role;
}
debugPrint(
'Role de l\'utilisateur: $roleValue');
debugPrint('Role de l\'utilisateur: $roleValue');
// Redirection simple basée sur le rôle
if (roleValue > 1) {
debugPrint(
'Redirection vers /admin (rôle > 1)');
debugPrint('Redirection vers /admin (rôle > 1)');
context.go('/admin');
} else {
debugPrint(
'Redirection vers /user (rôle = 1)');
debugPrint('Redirection vers /user (rôle = 1)');
context.go('/user');
}
} else if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Échec de la connexion. Vérifiez vos identifiants.'),
content: Text('Échec de la connexion. Vérifiez vos identifiants.'),
backgroundColor: Colors.red,
),
);
}
}
},
text: _isConnected
? 'Se connecter'
: 'Connexion Internet requise',
text: _isConnected ? 'Se connecter' : 'Connexion Internet requise',
isLoading: userRepository.isLoading,
),
const SizedBox(height: 24),
@@ -1047,8 +950,7 @@ class _LoginPageState extends State<LoginPage> {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
.hasMatch(value)) {
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Veuillez entrer un email valide';
}
return null;
@@ -1105,10 +1007,8 @@ class _LoginPageState extends State<LoginPage> {
// Si la réponse est 404, c'est peut-être un problème de route
if (response.statusCode == 404) {
// Essayer avec une URL alternative
final alternativeUrl =
'$baseUrl/api/index.php/lostpassword';
print(
'Tentative avec URL alternative: $alternativeUrl');
final alternativeUrl = '$baseUrl/api/index.php/lostpassword';
print('Tentative avec URL alternative: $alternativeUrl');
final alternativeResponse = await http.post(
Uri.parse(alternativeUrl),
@@ -1118,10 +1018,8 @@ class _LoginPageState extends State<LoginPage> {
}),
);
print(
'Réponse alternative reçue: ${alternativeResponse.statusCode}');
print(
'Corps de la réponse alternative: ${alternativeResponse.body}');
print('Réponse alternative reçue: ${alternativeResponse.statusCode}');
print('Corps de la réponse alternative: ${alternativeResponse.body}');
// Si la réponse alternative est un succès, utiliser cette réponse
if (alternativeResponse.statusCode == 200) {
@@ -1129,14 +1027,12 @@ class _LoginPageState extends State<LoginPage> {
}
}
} catch (e) {
print(
'Erreur lors de l\'envoi de la requête: $e');
print('Erreur lors de l\'envoi de la requête: $e');
throw Exception('Erreur de connexion: $e');
}
// Traiter la réponse
if (response != null &&
response.statusCode == 200) {
if (response.statusCode == 200) {
// Modifier le contenu de la boîte de dialogue pour afficher le message de succès
setState(() {
isLoading = false;
@@ -1148,7 +1044,7 @@ class _LoginPageState extends State<LoginPage> {
barrierDismissible: false,
builder: (BuildContext context) {
// Fermer automatiquement la boîte de dialogue après 2 secondes
Future.delayed(Duration(seconds: 2), () {
Future.delayed(const Duration(seconds: 2), () {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
@@ -1180,16 +1076,13 @@ class _LoginPageState extends State<LoginPage> {
// Afficher un message d'erreur
final responseData = json.decode(response.body);
throw Exception(responseData['message'] ??
'Erreur lors de la récupération du mot de passe');
throw Exception(responseData['message'] ?? 'Erreur lors de la récupération du mot de passe');
}
} catch (e) {
// Afficher un message d'erreur
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e
.toString()
.contains('Exception:')
content: Text(e.toString().contains('Exception:')
? e.toString().split('Exception: ')[1]
: 'Erreur lors de la récupération du mot de passe'),
backgroundColor: Colors.red,
@@ -1204,21 +1097,20 @@ class _LoginPageState extends State<LoginPage> {
}
}
},
child: isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text('Recevoir un nouveau mot de passe'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text('Recevoir un nouveau mot de passe'),
),
],
);

View File

@@ -73,10 +73,8 @@ class _RegisterPageState extends State<RegisterPage> {
final String _hiddenToken = DateTime.now().millisecondsSinceEpoch.toString();
// Valeurs pour le captcha simple
final int _captchaNum1 =
2 + (DateTime.now().second % 5); // Nombre entre 2 et 6
final int _captchaNum2 =
3 + (DateTime.now().minute % 4); // Nombre entre 3 et 6
final int _captchaNum1 = 2 + (DateTime.now().second % 5); // Nombre entre 2 et 6
final int _captchaNum2 = 3 + (DateTime.now().minute % 4); // Nombre entre 3 et 6
// État de la connexion Internet et de la plateforme
bool _isConnected = false;
@@ -102,9 +100,7 @@ class _RegisterPageState extends State<RegisterPage> {
// Fallback sur la version du AppInfoService si elle existe
if (mounted) {
setState(() {
_appVersion = AppInfoService.fullVersion
.split(' ')
.last; // Extraire juste le numéro
_appVersion = AppInfoService.fullVersion.split(' ').last; // Extraire juste le numéro
});
}
}
@@ -168,8 +164,7 @@ class _RegisterPageState extends State<RegisterPage> {
try {
// Utiliser l'API interne de geosector pour récupérer les villes par code postal
final baseUrl = Uri
.base.origin; // Récupère l'URL de base (ex: https://app.geosector.fr)
final baseUrl = Uri.base.origin; // Récupère l'URL de base (ex: https://app.geosector.fr)
final apiUrl = '$baseUrl/api/villes?code_postal=$postalCode';
final response = await http.get(
@@ -251,7 +246,7 @@ class _RegisterPageState extends State<RegisterPage> {
),
child: CustomPaint(
painter: DotsPainter(),
child: Container(width: double.infinity, height: double.infinity),
child: const SizedBox(width: double.infinity, height: double.infinity),
),
),
SafeArea(
@@ -282,8 +277,7 @@ class _RegisterPageState extends State<RegisterPage> {
Text(
'Enregistrez votre amicale sur GeoSector',
style: theme.textTheme.bodyLarge?.copyWith(
color:
theme.colorScheme.onBackground.withOpacity(0.7),
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
@@ -295,8 +289,7 @@ class _RegisterPageState extends State<RegisterPage> {
if (mounted && _isConnected != isConnected) {
setState(() {
_isConnected = isConnected;
_connectionType =
connectivityService.connectionType;
_connectionType = connectivityService.connectionType;
});
}
},
@@ -343,8 +336,7 @@ class _RegisterPageState extends State<RegisterPage> {
if (_isConnected && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Connexion Internet $_connectionType détectée.'),
content: Text('Connexion Internet $_connectionType détectée.'),
backgroundColor: Colors.green,
),
);
@@ -396,8 +388,7 @@ class _RegisterPageState extends State<RegisterPage> {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
.hasMatch(value)) {
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return 'Veuillez entrer un email valide';
}
return null;
@@ -424,8 +415,7 @@ class _RegisterPageState extends State<RegisterPage> {
CustomTextField(
controller: _postalCodeController,
label: 'Code postal de l\'amicale',
hintText:
'Entrez le code postal de votre amicale',
hintText: 'Entrez le code postal de votre amicale',
prefixIcon: Icons.location_on_outlined,
keyboardType: TextInputType.number,
isRequired: true,
@@ -453,13 +443,12 @@ class _RegisterPageState extends State<RegisterPage> {
children: [
Text(
'Commune de l\'amicale',
style:
theme.textTheme.titleSmall?.copyWith(
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w500,
color: theme.colorScheme.onBackground,
color: theme.colorScheme.onSurface,
),
),
Text(
const Text(
'',
style: TextStyle(
color: Colors.red,
@@ -484,8 +473,7 @@ class _RegisterPageState extends State<RegisterPage> {
),
child: _isLoadingCities
? const Padding(
padding: EdgeInsets.symmetric(
vertical: 16),
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(
child: CircularProgressIndicator(),
),
@@ -497,20 +485,16 @@ class _RegisterPageState extends State<RegisterPage> {
Icons.location_city_outlined,
color: theme.colorScheme.primary,
),
hintText: _postalCodeController
.text.length <
3
hintText: _postalCodeController.text.length < 3
? 'Entrez d\'abord au moins 3 chiffres du code postal'
: _cities.isEmpty
? 'Aucune commune trouvée pour ce code postal'
: 'Sélectionnez une commune',
border: OutlineInputBorder(
borderRadius:
BorderRadius.circular(12),
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding:
const EdgeInsets.symmetric(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
@@ -528,18 +512,13 @@ class _RegisterPageState extends State<RegisterPage> {
// Mettre à jour le code postal avec celui de la ville sélectionnée
if (newValue != null) {
// Désactiver temporairement le listener pour éviter une boucle infinie
_postalCodeController
.removeListener(
_onPostalCodeChanged);
_postalCodeController.removeListener(_onPostalCodeChanged);
// Mettre à jour le code postal
_postalCodeController.text =
newValue.postalCode;
_postalCodeController.text = newValue.postalCode;
// Réactiver le listener
_postalCodeController
.addListener(
_onPostalCodeChanged);
_postalCodeController.addListener(_onPostalCodeChanged);
}
});
},
@@ -574,8 +553,7 @@ class _RegisterPageState extends State<RegisterPage> {
const SizedBox(height: 8),
CustomTextField(
controller: _captchaController,
label:
'Combien font $_captchaNum1 + $_captchaNum2 ?',
label: 'Combien font $_captchaNum1 + $_captchaNum2 ?',
hintText: 'Entrez le résultat',
prefixIcon: Icons.security,
keyboardType: TextInputType.number,
@@ -612,43 +590,30 @@ class _RegisterPageState extends State<RegisterPage> {
// Bouton d'inscription
CustomButton(
onPressed: (_isLoading ||
(_isMobile && !_isConnected))
onPressed: (_isLoading || (_isMobile && !_isConnected))
? null
: () async {
if (_formKey.currentState!.validate()) {
// Vérifier la connexion Internet avant de soumettre
// Utiliser l'instance globale de connectivityService définie dans app.dart
await connectivityService
.checkConnectivity();
await connectivityService.checkConnectivity();
if (!connectivityService.isConnected) {
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text(
'Aucune connexion Internet. L\'inscription nécessite une connexion active.'),
backgroundColor:
theme.colorScheme.error,
duration:
const Duration(seconds: 3),
content: const Text('Aucune connexion Internet. L\'inscription nécessite une connexion active.'),
backgroundColor: theme.colorScheme.error,
duration: const Duration(seconds: 3),
action: SnackBarAction(
label: 'Réessayer',
onPressed: () async {
await connectivityService
.checkConnectivity();
if (connectivityService
.isConnected &&
mounted) {
ScaffoldMessenger.of(
context)
.showSnackBar(
await connectivityService.checkConnectivity();
if (connectivityService.isConnected && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Connexion Internet ${connectivityService.connectionType} détectée.'),
backgroundColor:
Colors.green,
content: Text('Connexion Internet ${connectivityService.connectionType} détectée.'),
backgroundColor: Colors.green,
),
);
}
@@ -660,15 +625,11 @@ class _RegisterPageState extends State<RegisterPage> {
return;
}
// Vérifier que le captcha est correct
final int? captchaAnswer = int.tryParse(
_captchaController.text);
if (captchaAnswer !=
_captchaNum1 + _captchaNum2) {
ScaffoldMessenger.of(context)
.showSnackBar(
final int? captchaAnswer = int.tryParse(_captchaController.text);
if (captchaAnswer != _captchaNum1 + _captchaNum2) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'La vérification de sécurité a échoué. Veuillez réessayer.'),
content: Text('La vérification de sécurité a échoué. Veuillez réessayer.'),
backgroundColor: Colors.red,
),
);
@@ -679,16 +640,11 @@ class _RegisterPageState extends State<RegisterPage> {
final Map<String, dynamic> formData = {
'email': _emailController.text.trim(),
'name': _nameController.text.trim(),
'amicale_name': _amicaleNameController
.text
.trim(),
'postal_code':
_postalCodeController.text,
'city_name':
_selectedCity?.name ?? '',
'amicale_name': _amicaleNameController.text.trim(),
'postal_code': _postalCodeController.text,
'city_name': _selectedCity?.name ?? '',
'captcha_answer': captchaAnswer,
'captcha_expected':
_captchaNum1 + _captchaNum2,
'captcha_expected': _captchaNum1 + _captchaNum2,
'token': _hiddenToken,
};
@@ -700,14 +656,12 @@ class _RegisterPageState extends State<RegisterPage> {
try {
// Envoyer les données à l'API
final baseUrl = Uri.base.origin;
final apiUrl =
'$baseUrl/api/register';
final apiUrl = '$baseUrl/api/register';
final response = await http.post(
Uri.parse(apiUrl),
headers: {
'Content-Type':
'application/json',
'Content-Type': 'application/json',
},
body: json.encode(formData),
);
@@ -718,34 +672,23 @@ class _RegisterPageState extends State<RegisterPage> {
});
// Traiter la réponse
if (response.statusCode == 200 ||
response.statusCode == 201) {
final responseData =
json.decode(response.body);
if (response.statusCode == 200 || response.statusCode == 201) {
final responseData = json.decode(response.body);
// Vérifier si la réponse indique un succès
final bool isSuccess =
responseData['success'] ==
true ||
responseData['status'] ==
'success';
final bool isSuccess = responseData['success'] == true || responseData['status'] == 'success';
// Récupérer le message de la réponse
final String message = responseData[
'message'] ??
(isSuccess
? 'Inscription réussie !'
: 'Échec de l\'inscription. Veuillez réessayer.');
final String message = responseData['message'] ??
(isSuccess ? 'Inscription réussie !' : 'Échec de l\'inscription. Veuillez réessayer.');
if (isSuccess) {
if (mounted) {
// Afficher une boîte de dialogue de succès
showDialog(
context: context,
barrierDismissible:
false, // L'utilisateur doit cliquer sur OK
builder:
(BuildContext context) {
barrierDismissible: false, // L'utilisateur doit cliquer sur OK
builder: (BuildContext context) {
return AlertDialog(
title: const Row(
children: [
@@ -754,84 +697,50 @@ class _RegisterPageState extends State<RegisterPage> {
color: Colors.green,
),
SizedBox(width: 10),
Text(
'Inscription réussie'),
Text('Inscription réussie'),
],
),
content: Column(
mainAxisSize:
MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment
.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Votre demande d\'inscription a été enregistrée avec succès.',
style: theme
.textTheme
.bodyLarge,
style: theme.textTheme.bodyLarge,
),
SizedBox(height: 16),
const SizedBox(height: 16),
Text(
'Vous allez recevoir un email contenant :',
style: theme
.textTheme
.bodyMedium,
style: theme.textTheme.bodyMedium,
),
SizedBox(height: 8),
const SizedBox(height: 8),
Row(
crossAxisAlignment:
CrossAxisAlignment
.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons
.arrow_right,
size: 20,
color: theme
.colorScheme
.primary),
const SizedBox(
width: 4),
Expanded(
child: Text(
'Votre identifiant de connexion'),
Icon(Icons.arrow_right, size: 20, color: theme.colorScheme.primary),
const SizedBox(width: 4),
const Expanded(
child: Text('Votre identifiant de connexion'),
),
],
),
SizedBox(height: 4),
const SizedBox(height: 4),
Row(
crossAxisAlignment:
CrossAxisAlignment
.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons
.arrow_right,
size: 20,
color: theme
.colorScheme
.primary),
SizedBox(
width: 4),
Expanded(
child: Text(
'Un lien pour définir votre mot de passe'),
Icon(Icons.arrow_right, size: 20, color: theme.colorScheme.primary),
const SizedBox(width: 4),
const Expanded(
child: Text('Un lien pour définir votre mot de passe'),
),
],
),
SizedBox(height: 16),
const SizedBox(height: 16),
Text(
'Vérifiez votre boîte de réception et vos spams.',
style: TextStyle(
fontStyle:
FontStyle
.italic,
color: theme
.colorScheme
.onSurface
.withOpacity(
0.7),
fontStyle: FontStyle.italic,
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
),
],
@@ -839,25 +748,15 @@ class _RegisterPageState extends State<RegisterPage> {
actions: [
TextButton(
onPressed: () {
Navigator.of(
context)
.pop();
Navigator.of(context).pop();
// Rediriger vers la page de connexion
context
.go('/login');
context.go('/login');
},
child: Text('OK'),
style: TextButton
.styleFrom(
foregroundColor:
theme
.colorScheme
.primary,
textStyle: TextStyle(
fontWeight:
FontWeight
.bold),
style: TextButton.styleFrom(
foregroundColor: theme.colorScheme.primary,
textStyle: const TextStyle(fontWeight: FontWeight.bold),
),
child: const Text('OK'),
),
],
);
@@ -870,21 +769,16 @@ class _RegisterPageState extends State<RegisterPage> {
// Afficher un message d'erreur plus visible
showDialog(
context: context,
builder:
(BuildContext context) {
builder: (BuildContext context) {
return AlertDialog(
title: const Text(
'Erreur d\'inscription'),
title: const Text('Erreur d\'inscription'),
content: Text(message),
actions: [
TextButton(
onPressed: () {
Navigator.of(
context)
.pop();
Navigator.of(context).pop();
},
child:
const Text('OK'),
child: const Text('OK'),
),
],
);
@@ -892,8 +786,7 @@ class _RegisterPageState extends State<RegisterPage> {
);
// Afficher également un SnackBar
ScaffoldMessenger.of(context)
.showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
@@ -904,11 +797,9 @@ class _RegisterPageState extends State<RegisterPage> {
} else {
// Gérer les erreurs HTTP
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Erreur ${response.statusCode}: ${response.reasonPhrase ?? "Échec de l\'inscription"}'),
content: Text('Erreur ${response.statusCode}: ${response.reasonPhrase ?? "Échec de l'inscription"}'),
backgroundColor: Colors.red,
),
);
@@ -922,11 +813,9 @@ class _RegisterPageState extends State<RegisterPage> {
// Gérer les exceptions
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Erreur: ${e.toString()}'),
content: Text('Erreur: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
@@ -934,9 +823,7 @@ class _RegisterPageState extends State<RegisterPage> {
}
}
},
text: (_isMobile && !_isConnected)
? 'Connexion Internet requise'
: 'Enregistrer mon amicale',
text: (_isMobile && !_isConnected) ? 'Connexion Internet requise' : 'Enregistrer mon amicale',
isLoading: _isLoading,
),
const SizedBox(height: 24),

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,
);
},
);
}
}