- Ajout système complet de gestion des secteurs avec contours géographiques - Import des contours départementaux depuis GeoJSON - API REST pour la gestion des secteurs (/api/sectors) - Service de géolocalisation pour déterminer les secteurs - Migration base de données avec tables x_departements_contours et sectors_adresses - Interface Flutter pour visualisation et gestion des secteurs - Ajout thème sombre dans l'application - Corrections diverses et optimisations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
368 lines
12 KiB
Dart
Executable File
368 lines
12 KiB
Dart
Executable File
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
|
import 'package:flutter/material.dart';
|
|
import 'package:hive_flutter/hive_flutter.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';
|
|
import 'user_statistics_page.dart';
|
|
import 'user_history_page.dart';
|
|
import 'user_communication_page.dart';
|
|
import 'user_map_page.dart';
|
|
|
|
class UserDashboardPage extends StatefulWidget {
|
|
const UserDashboardPage({super.key});
|
|
|
|
@override
|
|
State<UserDashboardPage> createState() => _UserDashboardPageState();
|
|
}
|
|
|
|
class _UserDashboardPageState extends State<UserDashboardPage> {
|
|
int _selectedIndex = 0;
|
|
|
|
// Liste des pages à afficher
|
|
late final List<Widget> _pages;
|
|
|
|
// Référence à la boîte Hive pour les paramètres
|
|
late Box _settingsBox;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_pages = [
|
|
const UserDashboardHomePage(),
|
|
const UserStatisticsPage(),
|
|
const UserHistoryPage(),
|
|
const UserCommunicationPage(),
|
|
const UserMapPage(),
|
|
];
|
|
|
|
// Initialiser et charger les paramètres
|
|
_initSettings();
|
|
}
|
|
|
|
// Initialiser la boîte de paramètres et charger les préférences
|
|
Future<void> _initSettings() async {
|
|
try {
|
|
// Ouvrir la boîte de paramètres si elle n'est pas déjà ouverte
|
|
if (!Hive.isBoxOpen(AppKeys.settingsBoxName)) {
|
|
_settingsBox = await Hive.openBox(AppKeys.settingsBoxName);
|
|
} else {
|
|
_settingsBox = Hive.box(AppKeys.settingsBoxName);
|
|
}
|
|
|
|
// Charger l'index de page sélectionné
|
|
final savedIndex = _settingsBox.get('selectedPageIndex');
|
|
if (savedIndex != null &&
|
|
savedIndex is int &&
|
|
savedIndex >= 0 &&
|
|
savedIndex < _pages.length) {
|
|
setState(() {
|
|
_selectedIndex = savedIndex;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du chargement des paramètres: $e');
|
|
}
|
|
}
|
|
|
|
// Sauvegarder les paramètres utilisateur
|
|
void _saveSettings() {
|
|
try {
|
|
// Sauvegarder l'index de page sélectionné
|
|
_settingsBox.put('selectedPageIndex', _selectedIndex);
|
|
} catch (e) {
|
|
debugPrint('Erreur lors de la sauvegarde des paramètres: $e');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Utiliser l'instance globale définie dans app.dart
|
|
final hasOperation = userRepository.getCurrentOperation() != null;
|
|
final hasSectors = userRepository.getUserSectors().isNotEmpty;
|
|
final isStandardUser = userRepository.currentUser != null &&
|
|
userRepository.currentUser!.role ==
|
|
'1'; // Rôle 1 = utilisateur standard
|
|
|
|
// Si l'utilisateur est standard et n'a pas d'opération assignée ou n'a pas de secteur, afficher un message spécial
|
|
final bool shouldShowNoOperationMessage = isStandardUser && !hasOperation;
|
|
final bool shouldShowNoSectorMessage = isStandardUser && !hasSectors;
|
|
|
|
// 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',
|
|
),
|
|
],
|
|
showNewPassageButton: false,
|
|
body: _buildNoOperationMessage(context),
|
|
);
|
|
}
|
|
|
|
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
|
|
Widget _buildNoOperationMessage(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return Center(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(24),
|
|
constraints: const BoxConstraints(maxWidth: 500),
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.surface,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: theme.shadowColor.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.warning_amber_rounded,
|
|
size: 80,
|
|
color: theme.colorScheme.error,
|
|
),
|
|
const SizedBox(height: 24),
|
|
Text(
|
|
'Aucune opération assignée',
|
|
style: theme.textTheme.headlineSmall?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Vous n\'avez pas encore été affecté à une opération. Veuillez contacter votre administrateur pour obtenir un accès.',
|
|
style: theme.textTheme.bodyLarge?.copyWith(
|
|
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Message pour les utilisateurs sans secteur assigné
|
|
Widget _buildNoSectorMessage(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return Center(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(24),
|
|
constraints: const BoxConstraints(maxWidth: 500),
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.surface,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: theme.shadowColor.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.map_outlined,
|
|
size: 80,
|
|
color: theme.colorScheme.error,
|
|
),
|
|
const SizedBox(height: 24),
|
|
Text(
|
|
'Aucun secteur assigné',
|
|
style: theme.textTheme.headlineSmall?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Vous n\'êtes affecté sur aucun secteur. Contactez votre administrateur pour qu\'il vous en affecte au moins un.',
|
|
style: theme.textTheme.bodyLarge?.copyWith(
|
|
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Affiche le formulaire de passage
|
|
void _showPassageForm(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => Dialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Container(
|
|
constraints: const BoxConstraints(
|
|
maxWidth: 600,
|
|
maxHeight: 700,
|
|
),
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// 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,
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
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);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 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,
|
|
// ),
|
|
// );
|
|
// }
|
|
}
|
|
}
|