Amélioration de la splash_page et du login

This commit is contained in:
d6soft
2025-06-04 16:51:40 +02:00
parent 8c9e9a21c4
commit 41f1db1169
228 changed files with 366459 additions and 6183 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

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