Amélioration de la splash_page et du login
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,12 @@
|
||||
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:geosector_app/core/repositories/user_repository.dart';
|
||||
import 'package:geosector_app/core/theme/app_theme.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/presentation/widgets/dashboard_layout.dart';
|
||||
import 'package:geosector_app/presentation/widgets/passages/passage_form.dart';
|
||||
|
||||
// Import des pages utilisateur
|
||||
import 'user_dashboard_home_page.dart';
|
||||
@@ -94,73 +94,85 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
|
||||
final bool shouldShowNoOperationMessage = isStandardUser && !hasOperation;
|
||||
final bool shouldShowNoSectorMessage = isStandardUser && !hasSectors;
|
||||
|
||||
// Définir les actions supplémentaires pour l'AppBar
|
||||
List<Widget>? additionalActions;
|
||||
if (shouldShowNoOperationMessage || shouldShowNoSectorMessage) {
|
||||
additionalActions = [
|
||||
// Bouton de déconnexion uniquement si l'utilisateur n'a pas d'opération
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.logout, color: Colors.white),
|
||||
label: const Text('Se déconnecter',
|
||||
style: TextStyle(color: Colors.white)),
|
||||
onPressed: () async {
|
||||
// Utiliser directement userRepository pour la déconnexion
|
||||
await userRepository.logoutWithUI(context);
|
||||
// La redirection est gérée dans logoutWithUI
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: AppTheme.accentColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
// Si l'utilisateur n'a pas d'opération ou de secteur, utiliser DashboardLayout avec un body spécial
|
||||
if (shouldShowNoOperationMessage) {
|
||||
return DashboardLayout(
|
||||
title: 'GEOSECTOR',
|
||||
selectedIndex: 0, // Index par défaut
|
||||
onDestinationSelected: (index) {
|
||||
// Ne rien faire car l'utilisateur ne peut pas naviguer
|
||||
},
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.warning_outlined),
|
||||
selectedIcon: Icon(Icons.warning),
|
||||
label: 'Accès restreint',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16), // Espacement à droite
|
||||
];
|
||||
],
|
||||
showNewPassageButton: false,
|
||||
body: _buildNoOperationMessage(context),
|
||||
);
|
||||
}
|
||||
|
||||
return shouldShowNoOperationMessage
|
||||
? _buildNoOperationMessage(context)
|
||||
: (shouldShowNoSectorMessage
|
||||
? _buildNoSectorMessage(context)
|
||||
: DashboardLayout(
|
||||
title: 'GEOSECTOR',
|
||||
selectedIndex: _selectedIndex,
|
||||
onDestinationSelected: (index) {
|
||||
setState(() {
|
||||
_selectedIndex = index;
|
||||
_saveSettings(); // Sauvegarder l'index de page sélectionné
|
||||
});
|
||||
},
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.dashboard_outlined),
|
||||
selectedIcon: Icon(Icons.dashboard),
|
||||
label: 'Tableau de bord',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.bar_chart_outlined),
|
||||
selectedIcon: Icon(Icons.bar_chart),
|
||||
label: 'Stats',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.history_outlined),
|
||||
selectedIcon: Icon(Icons.history),
|
||||
label: 'Historique',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.chat_outlined),
|
||||
selectedIcon: Icon(Icons.chat),
|
||||
label: 'Messages',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.map_outlined),
|
||||
selectedIcon: Icon(Icons.map),
|
||||
label: 'Carte',
|
||||
),
|
||||
],
|
||||
additionalActions: additionalActions,
|
||||
onNewPassagePressed: () => _showPassageForm(context),
|
||||
body: _pages[_selectedIndex],
|
||||
));
|
||||
if (shouldShowNoSectorMessage) {
|
||||
return DashboardLayout(
|
||||
title: 'GEOSECTOR',
|
||||
selectedIndex: 0, // Index par défaut
|
||||
onDestinationSelected: (index) {
|
||||
// Ne rien faire car l'utilisateur ne peut pas naviguer
|
||||
},
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.warning_outlined),
|
||||
selectedIcon: Icon(Icons.warning),
|
||||
label: 'Accès restreint',
|
||||
),
|
||||
],
|
||||
showNewPassageButton: false,
|
||||
body: _buildNoSectorMessage(context),
|
||||
);
|
||||
}
|
||||
|
||||
// Utilisateur normal avec accès complet
|
||||
return DashboardLayout(
|
||||
title: 'GEOSECTOR',
|
||||
selectedIndex: _selectedIndex,
|
||||
onDestinationSelected: (index) {
|
||||
setState(() {
|
||||
_selectedIndex = index;
|
||||
_saveSettings(); // Sauvegarder l'index de page sélectionné
|
||||
});
|
||||
},
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.dashboard_outlined),
|
||||
selectedIcon: Icon(Icons.dashboard),
|
||||
label: 'Tableau de bord',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.bar_chart_outlined),
|
||||
selectedIcon: Icon(Icons.bar_chart),
|
||||
label: 'Stats',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.history_outlined),
|
||||
selectedIcon: Icon(Icons.history),
|
||||
label: 'Historique',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.chat_outlined),
|
||||
selectedIcon: Icon(Icons.chat),
|
||||
label: 'Messages',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.map_outlined),
|
||||
selectedIcon: Icon(Icons.map),
|
||||
label: 'Carte',
|
||||
),
|
||||
],
|
||||
onNewPassagePressed: () => _showPassageForm(context),
|
||||
body: _pages[_selectedIndex],
|
||||
);
|
||||
}
|
||||
|
||||
// Message pour les utilisateurs sans opération assignée
|
||||
@@ -269,110 +281,90 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(
|
||||
'Nouveau passage',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
builder: (context) => Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 600,
|
||||
maxHeight: 700,
|
||||
),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Adresse',
|
||||
prefixIcon: const Icon(Icons.location_on),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
// En-tête de la modale
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Nouveau passage',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<int>(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Type de passage',
|
||||
prefixIcon: const Icon(Icons.category),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: 1,
|
||||
child: Text('Effectué'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 2,
|
||||
child: Text('À finaliser'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 3,
|
||||
child: Text('Refusé'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 4,
|
||||
child: Text('Don'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 5,
|
||||
child: Text('Lot'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 6,
|
||||
child: Text('Maison vide'),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Commentaire',
|
||||
prefixIcon: const Icon(Icons.comment),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Formulaire de passage
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: PassageForm(
|
||||
onSubmit: (formData) {
|
||||
// Traiter les données du formulaire
|
||||
_handlePassageSubmission(context, formData);
|
||||
},
|
||||
),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
'Annuler',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Enregistrer le passage
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Passage enregistré avec succès'),
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
foregroundColor: theme.colorScheme.onPrimary,
|
||||
),
|
||||
child: const Text('Enregistrer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Traiter la soumission du formulaire de passage
|
||||
void _handlePassageSubmission(
|
||||
BuildContext context, Map<String, dynamic> formData) {
|
||||
// Fermer la modale
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Ici vous pouvez traiter les données du formulaire
|
||||
// Par exemple, les envoyer au repository ou à un service
|
||||
|
||||
// Pour l'instant, afficher un message de succès
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content:
|
||||
Text('Passage enregistré avec succès pour ${formData['adresse']}'),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
|
||||
// TODO: Intégrer avec votre logique métier
|
||||
// Exemple :
|
||||
// try {
|
||||
// await passageRepository.createPassage(formData);
|
||||
// // Rafraîchir les données si nécessaire
|
||||
// } catch (e) {
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// SnackBar(
|
||||
// content: Text('Erreur lors de l\'enregistrement: $e'),
|
||||
// backgroundColor: Theme.of(context).colorScheme.error,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:geosector_app/core/theme/app_theme.dart';
|
||||
import 'package:geosector_app/presentation/widgets/charts/charts.dart';
|
||||
import 'package:geosector_app/core/repositories/user_repository.dart';
|
||||
|
||||
class UserStatisticsPage extends StatefulWidget {
|
||||
const UserStatisticsPage({super.key});
|
||||
@@ -58,8 +55,9 @@ class _UserStatisticsPageState extends State<UserStatisticsPage> {
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Résumé par type de règlement
|
||||
_buildPaymentTypeSummary(theme, isDesktop),
|
||||
// Résumé par type de règlement
|
||||
_buildPaymentTypeSummary(theme, isDesktop),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -363,219 +361,34 @@ class _UserStatisticsPageState extends State<UserStatisticsPage> {
|
||||
|
||||
// Construction du résumé par type de passage
|
||||
Widget _buildPassageTypeSummary(ThemeData theme, bool isDesktop) {
|
||||
// Dans une implémentation réelle, ces données seraient filtrées par secteur
|
||||
// en fonction de _selectedSectorId
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Répartition par type de passage',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
// Graphique circulaire
|
||||
Expanded(
|
||||
flex: isDesktop ? 1 : 2,
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
child: PassagePieChart(
|
||||
passagesByType: {
|
||||
1: 60, // Effectués
|
||||
2: 15, // À finaliser
|
||||
3: 10, // Refusés
|
||||
4: 8, // Dons
|
||||
5: 5, // Lots
|
||||
6: 2, // Maisons vides
|
||||
},
|
||||
size: 140,
|
||||
labelSize: 12,
|
||||
showPercentage: true,
|
||||
showIcons: false, // Désactiver les icônes
|
||||
isDonut: true, // Activer le format donut
|
||||
innerRadius: '50%' // Rayon interne du donut
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Légende
|
||||
if (isDesktop)
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildLegendItem(
|
||||
'Effectués', '60%', const Color(0xFF4CAF50)),
|
||||
_buildLegendItem(
|
||||
'À finaliser', '15%', const Color(0xFFFF9800)),
|
||||
_buildLegendItem(
|
||||
'Refusés', '10%', const Color(0xFFF44336)),
|
||||
_buildLegendItem('Dons', '8%', const Color(0xFF03A9F4)),
|
||||
_buildLegendItem('Lots', '5%', const Color(0xFF0D47A1)),
|
||||
_buildLegendItem(
|
||||
'Maisons vides', '2%', const Color(0xFF9E9E9E)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!isDesktop)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
_buildLegendItem('Effectués', '60%', const Color(0xFF4CAF50)),
|
||||
_buildLegendItem(
|
||||
'À finaliser', '15%', const Color(0xFFFF9800)),
|
||||
_buildLegendItem('Refusés', '10%', const Color(0xFFF44336)),
|
||||
_buildLegendItem('Dons', '8%', const Color(0xFF03A9F4)),
|
||||
_buildLegendItem('Lots', '5%', const Color(0xFF0D47A1)),
|
||||
_buildLegendItem(
|
||||
'Maisons vides', '2%', const Color(0xFF9E9E9E)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
return PassageSummaryCard(
|
||||
title: 'Répartition par type de passage',
|
||||
titleColor: theme.colorScheme.primary,
|
||||
titleIcon: Icons.pie_chart,
|
||||
height: 300,
|
||||
useValueListenable: true,
|
||||
userId: userRepository.getCurrentUser()?.id,
|
||||
showAllPassages: false,
|
||||
excludePassageTypes: const [2], // Exclure "À finaliser"
|
||||
isDesktop: isDesktop,
|
||||
);
|
||||
}
|
||||
|
||||
// Construction du résumé par type de règlement
|
||||
Widget _buildPaymentTypeSummary(ThemeData theme, bool isDesktop) {
|
||||
// Dans une implémentation réelle, ces données seraient filtrées par secteur
|
||||
// en fonction de _selectedSectorId
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Répartition par type de règlement',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
// Graphique circulaire
|
||||
Expanded(
|
||||
flex: isDesktop ? 1 : 2,
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 2,
|
||||
centerSpaceRadius: 40,
|
||||
sections: [
|
||||
_buildPieChartSection(
|
||||
'Espèces', 30, const Color(0xFF4CAF50), 0),
|
||||
_buildPieChartSection(
|
||||
'Chèques', 45, const Color(0xFF2196F3), 1),
|
||||
_buildPieChartSection(
|
||||
'CB', 25, const Color(0xFFF44336), 2),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Légende
|
||||
if (isDesktop)
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildLegendItem(
|
||||
'Espèces', '30%', const Color(0xFF4CAF50)),
|
||||
_buildLegendItem(
|
||||
'Chèques', '45%', const Color(0xFF2196F3)),
|
||||
_buildLegendItem('CB', '25%', const Color(0xFFF44336)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!isDesktop)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
_buildLegendItem('Espèces', '30%', const Color(0xFF4CAF50)),
|
||||
_buildLegendItem('Chèques', '45%', const Color(0xFF2196F3)),
|
||||
_buildLegendItem('CB', '25%', const Color(0xFFF44336)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Construction d'une section de graphique circulaire
|
||||
PieChartSectionData _buildPieChartSection(
|
||||
String title, double value, Color color, int index) {
|
||||
return PieChartSectionData(
|
||||
color: color,
|
||||
value: value,
|
||||
title: '$value%',
|
||||
radius: 60,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Construction d'un élément de légende
|
||||
Widget _buildLegendItem(String title, String value, Color color) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget _buildPaymentTypeSummary(ThemeData theme, bool isDesktop) {
|
||||
return PaymentSummaryCard(
|
||||
title: 'Répartition par type de règlement',
|
||||
titleColor: AppTheme.accentColor,
|
||||
titleIcon: Icons.pie_chart,
|
||||
height: 300,
|
||||
useValueListenable: true,
|
||||
userId: userRepository.getCurrentUser()?.id,
|
||||
showAllPayments: false,
|
||||
isDesktop: isDesktop,
|
||||
backgroundIcon: Icons.euro_symbol,
|
||||
backgroundIconColor: Colors.blue,
|
||||
backgroundIconOpacity: 0.05,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user