feat: Gestion des secteurs et migration v3.0.4+304
- 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>
This commit is contained in:
37
app/lib/app.dart
Normal file → Executable file
37
app/lib/app.dart
Normal file → Executable file
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/core/theme/app_theme.dart';
|
||||
import 'package:geosector_app/core/services/theme_service.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:geosector_app/core/services/current_user_service.dart';
|
||||
@@ -13,6 +14,7 @@ import 'package:geosector_app/core/services/sync_service.dart';
|
||||
import 'package:geosector_app/core/services/connectivity_service.dart';
|
||||
import 'package:geosector_app/presentation/auth/splash_page.dart';
|
||||
import 'package:geosector_app/presentation/auth/login_page.dart';
|
||||
import 'package:geosector_app/presentation/auth/register_page.dart';
|
||||
import 'package:geosector_app/presentation/admin/admin_dashboard_page.dart';
|
||||
import 'package:geosector_app/presentation/user/user_dashboard_page.dart';
|
||||
|
||||
@@ -25,19 +27,25 @@ final membreRepository = MembreRepository();
|
||||
final amicaleRepository = AmicaleRepository();
|
||||
final syncService = SyncService(userRepository: userRepository);
|
||||
final connectivityService = ConnectivityService();
|
||||
final themeService = ThemeService.instance;
|
||||
|
||||
class GeosectorApp extends StatelessWidget {
|
||||
const GeosectorApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
title: 'GeoSector',
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
themeMode: ThemeMode.system,
|
||||
routerConfig: _createRouter(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
return AnimatedBuilder(
|
||||
animation: themeService,
|
||||
builder: (context, child) {
|
||||
return MaterialApp.router(
|
||||
title: 'GeoSector',
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
themeMode: themeService.themeMode,
|
||||
routerConfig: _createRouter(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,8 +58,12 @@ class GeosectorApp extends StatelessWidget {
|
||||
path: '/',
|
||||
name: 'splash',
|
||||
builder: (context, state) {
|
||||
debugPrint('GoRoute: Affichage de SplashPage');
|
||||
return const SplashPage();
|
||||
// Récupérer les paramètres de query pour redirection automatique
|
||||
final action = state.uri.queryParameters['action'];
|
||||
final type = state.uri.queryParameters['type'];
|
||||
|
||||
debugPrint('GoRoute: Affichage de SplashPage avec action=$action, type=$type');
|
||||
return SplashPage(action: action, type: type);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -87,12 +99,7 @@ class GeosectorApp extends StatelessWidget {
|
||||
name: 'register',
|
||||
builder: (context, state) {
|
||||
debugPrint('GoRoute: Affichage de RegisterPage');
|
||||
// Retournez votre page d'inscription ici
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Text('Page d\'inscription - À implémenter'),
|
||||
),
|
||||
);
|
||||
return const RegisterPage();
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
|
||||
0
app/lib/chat/README.md
Normal file → Executable file
0
app/lib/chat/README.md
Normal file → Executable file
1
app/lib/chat/chat.dart
Normal file → Executable file
1
app/lib/chat/chat.dart
Normal file → Executable file
@@ -2,6 +2,7 @@
|
||||
///
|
||||
/// Ce fichier centralise les exportations du module chat
|
||||
/// pour faciliter l'importation dans d'autres parties de l'application
|
||||
library;
|
||||
|
||||
// Models
|
||||
export 'models/conversation_model.dart';
|
||||
|
||||
0
app/lib/chat/chat_updated.md
Normal file → Executable file
0
app/lib/chat/chat_updated.md
Normal file → Executable file
1
app/lib/chat/constants/chat_constants.dart
Normal file → Executable file
1
app/lib/chat/constants/chat_constants.dart
Normal file → Executable file
@@ -1,4 +1,5 @@
|
||||
/// Constantes spécifiques au module chat
|
||||
library;
|
||||
|
||||
class ChatConstants {
|
||||
// Types de conversations
|
||||
|
||||
8
app/lib/chat/example_integration/mqtt_integration_example.dart
Normal file → Executable file
8
app/lib/chat/example_integration/mqtt_integration_example.dart
Normal file → Executable file
@@ -55,7 +55,6 @@ class _MqttIntegrationExampleState extends State<MqttIntegrationExample> {
|
||||
_isInitialized = true;
|
||||
_status = 'Service MQTT initialisé';
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_status = 'Erreur : $e';
|
||||
@@ -127,7 +126,8 @@ class _MqttIntegrationExampleState extends State<MqttIntegrationExample> {
|
||||
'chat/user/${_getCurrentUserId()}/messages',
|
||||
{
|
||||
'type': 'chat_message',
|
||||
'messageId': 'test_${DateTime.now().millisecondsSinceEpoch}',
|
||||
'messageId':
|
||||
'test_${DateTime.now().millisecondsSinceEpoch}',
|
||||
'content': 'Message de test',
|
||||
'senderId': '999',
|
||||
'senderName': 'Système',
|
||||
@@ -159,8 +159,8 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: const MqttIntegrationExample(),
|
||||
return const MaterialApp(
|
||||
home: MqttIntegrationExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
0
app/lib/chat/models/anonymous_user_model.dart
Normal file → Executable file
0
app/lib/chat/models/anonymous_user_model.dart
Normal file → Executable file
0
app/lib/chat/models/audience_target_model.dart
Normal file → Executable file
0
app/lib/chat/models/audience_target_model.dart
Normal file → Executable file
0
app/lib/chat/models/chat_adapters.dart
Normal file → Executable file
0
app/lib/chat/models/chat_adapters.dart
Normal file → Executable file
0
app/lib/chat/models/chat_config.dart
Normal file → Executable file
0
app/lib/chat/models/chat_config.dart
Normal file → Executable file
0
app/lib/chat/models/conversation_model.dart
Normal file → Executable file
0
app/lib/chat/models/conversation_model.dart
Normal file → Executable file
0
app/lib/chat/models/message_model.dart
Normal file → Executable file
0
app/lib/chat/models/message_model.dart
Normal file → Executable file
0
app/lib/chat/models/notification_settings.dart
Normal file → Executable file
0
app/lib/chat/models/notification_settings.dart
Normal file → Executable file
0
app/lib/chat/models/participant_model.dart
Normal file → Executable file
0
app/lib/chat/models/participant_model.dart
Normal file → Executable file
8
app/lib/chat/pages/chat_page.dart
Normal file → Executable file
8
app/lib/chat/pages/chat_page.dart
Normal file → Executable file
@@ -32,7 +32,8 @@ class _ChatPageState extends State<ChatPage> {
|
||||
child: ConversationsList(
|
||||
onConversationSelected: (conversation) {
|
||||
setState(() {
|
||||
_selectedConversationId = 'conversation-id'; // TODO: obtenir l'ID de la conversation
|
||||
_selectedConversationId =
|
||||
'conversation-id'; // TODO: obtenir l'ID de la conversation
|
||||
});
|
||||
},
|
||||
),
|
||||
@@ -66,8 +67,9 @@ class _ChatPageState extends State<ChatPage> {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChatScreen(
|
||||
conversationId: 'conversation-id', // TODO: obtenir l'ID de la conversation
|
||||
builder: (context) => const ChatScreen(
|
||||
conversationId:
|
||||
'conversation-id', // TODO: obtenir l'ID de la conversation
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
0
app/lib/chat/repositories/chat_repository.dart
Normal file → Executable file
0
app/lib/chat/repositories/chat_repository.dart
Normal file → Executable file
0
app/lib/chat/scripts/chat_tables.sql
Normal file → Executable file
0
app/lib/chat/scripts/chat_tables.sql
Normal file → Executable file
0
app/lib/chat/scripts/mqtt_notification_sender.php
Normal file → Executable file
0
app/lib/chat/scripts/mqtt_notification_sender.php
Normal file → Executable file
0
app/lib/chat/scripts/send_notification.php
Normal file → Executable file
0
app/lib/chat/scripts/send_notification.php
Normal file → Executable file
28
app/lib/chat/services/chat_api_service.dart
Normal file → Executable file
28
app/lib/chat/services/chat_api_service.dart
Normal file → Executable file
@@ -1,6 +1,7 @@
|
||||
/// Service API pour la communication avec le backend du chat
|
||||
///
|
||||
/// Ce service gère toutes les requêtes HTTP vers l'API chat
|
||||
library;
|
||||
|
||||
class ChatApiService {
|
||||
final String baseUrl;
|
||||
@@ -18,19 +19,22 @@ class ChatApiService {
|
||||
}
|
||||
|
||||
/// Récupère les messages d'une conversation
|
||||
Future<Map<String, dynamic>> fetchMessages(String conversationId, {int page = 1, int limit = 50}) async {
|
||||
Future<Map<String, dynamic>> fetchMessages(String conversationId,
|
||||
{int page = 1, int limit = 50}) async {
|
||||
// TODO: Implémenter la requête HTTP
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
/// Crée une nouvelle conversation
|
||||
Future<Map<String, dynamic>> createConversation(Map<String, dynamic> data) async {
|
||||
Future<Map<String, dynamic>> createConversation(
|
||||
Map<String, dynamic> data) async {
|
||||
// TODO: Implémenter la requête HTTP
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
/// Envoie un message
|
||||
Future<Map<String, dynamic>> sendMessage(String conversationId, Map<String, dynamic> messageData) async {
|
||||
Future<Map<String, dynamic>> sendMessage(
|
||||
String conversationId, Map<String, dynamic> messageData) async {
|
||||
// TODO: Implémenter la requête HTTP
|
||||
throw UnimplementedError();
|
||||
}
|
||||
@@ -42,19 +46,22 @@ class ChatApiService {
|
||||
}
|
||||
|
||||
/// Ajoute un participant
|
||||
Future<Map<String, dynamic>> addParticipant(String conversationId, Map<String, dynamic> participantData) async {
|
||||
Future<Map<String, dynamic>> addParticipant(
|
||||
String conversationId, Map<String, dynamic> participantData) async {
|
||||
// TODO: Implémenter la requête HTTP
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
/// Retire un participant
|
||||
Future<Map<String, dynamic>> removeParticipant(String conversationId, String participantId) async {
|
||||
Future<Map<String, dynamic>> removeParticipant(
|
||||
String conversationId, String participantId) async {
|
||||
// TODO: Implémenter la requête HTTP
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
/// Crée un utilisateur anonyme
|
||||
Future<Map<String, dynamic>> createAnonymousUser({String? name, String? email}) async {
|
||||
Future<Map<String, dynamic>> createAnonymousUser(
|
||||
{String? name, String? email}) async {
|
||||
// TODO: Implémenter la requête HTTP
|
||||
throw UnimplementedError();
|
||||
}
|
||||
@@ -66,13 +73,15 @@ class ChatApiService {
|
||||
}
|
||||
|
||||
/// Crée une annonce
|
||||
Future<Map<String, dynamic>> createAnnouncement(Map<String, dynamic> data) async {
|
||||
Future<Map<String, dynamic>> createAnnouncement(
|
||||
Map<String, dynamic> data) async {
|
||||
// TODO: Implémenter la requête HTTP
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
/// Récupère les statistiques d'une annonce
|
||||
Future<Map<String, dynamic>> fetchAnnouncementStats(String conversationId) async {
|
||||
Future<Map<String, dynamic>> fetchAnnouncementStats(
|
||||
String conversationId) async {
|
||||
// TODO: Implémenter la requête HTTP
|
||||
throw UnimplementedError();
|
||||
}
|
||||
@@ -84,7 +93,8 @@ class ChatApiService {
|
||||
}
|
||||
|
||||
/// Met à jour une conversation
|
||||
Future<Map<String, dynamic>> updateConversation(String id, Map<String, dynamic> data) async {
|
||||
Future<Map<String, dynamic>> updateConversation(
|
||||
String id, Map<String, dynamic> data) async {
|
||||
// TODO: Implémenter la requête HTTP
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
0
app/lib/chat/services/notifications/README_MQTT.md
Normal file → Executable file
0
app/lib/chat/services/notifications/README_MQTT.md
Normal file → Executable file
43
app/lib/chat/services/notifications/chat_notification_service.dart
Normal file → Executable file
43
app/lib/chat/services/notifications/chat_notification_service.dart
Normal file → Executable file
@@ -3,17 +3,19 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Service de gestion des notifications chat
|
||||
///
|
||||
///
|
||||
/// Gère l'envoi et la réception des notifications pour le module chat
|
||||
|
||||
class ChatNotificationService {
|
||||
static final ChatNotificationService _instance = ChatNotificationService._internal();
|
||||
static final ChatNotificationService _instance =
|
||||
ChatNotificationService._internal();
|
||||
factory ChatNotificationService() => _instance;
|
||||
ChatNotificationService._internal();
|
||||
|
||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
|
||||
final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin();
|
||||
|
||||
final FlutterLocalNotificationsPlugin _localNotifications =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
// Callback pour les actions sur les notifications
|
||||
Function(String messageId)? onMessageTap;
|
||||
Function(Map<String, dynamic>)? onBackgroundMessage;
|
||||
@@ -22,13 +24,13 @@ class ChatNotificationService {
|
||||
Future<void> initialize() async {
|
||||
// Demander les permissions
|
||||
await _requestPermissions();
|
||||
|
||||
|
||||
// Initialiser les notifications locales
|
||||
await _initializeLocalNotifications();
|
||||
|
||||
|
||||
// Configurer les handlers de messages
|
||||
_configureFirebaseHandlers();
|
||||
|
||||
|
||||
// Obtenir le token du device
|
||||
await _initializeDeviceToken();
|
||||
}
|
||||
@@ -47,10 +49,11 @@ class ChatNotificationService {
|
||||
|
||||
/// Initialise les notifications locales
|
||||
Future<void> _initializeLocalNotifications() async {
|
||||
const AndroidInitializationSettings androidSettings =
|
||||
const AndroidInitializationSettings androidSettings =
|
||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
|
||||
final DarwinInitializationSettings iosSettings = DarwinInitializationSettings(
|
||||
|
||||
final DarwinInitializationSettings iosSettings =
|
||||
DarwinInitializationSettings(
|
||||
requestAlertPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestSoundPermission: true,
|
||||
@@ -72,10 +75,10 @@ class ChatNotificationService {
|
||||
void _configureFirebaseHandlers() {
|
||||
// Message reçu quand l'app est au premier plan
|
||||
FirebaseMessaging.onMessage.listen(_onForegroundMessage);
|
||||
|
||||
|
||||
// Message reçu quand l'app est en arrière-plan
|
||||
FirebaseMessaging.onMessageOpenedApp.listen(_onBackgroundMessageOpened);
|
||||
|
||||
|
||||
// Handler pour les messages en arrière-plan terminé
|
||||
FirebaseMessaging.onBackgroundMessage(_firebaseBackgroundHandler);
|
||||
}
|
||||
@@ -106,7 +109,8 @@ class ChatNotificationService {
|
||||
required String body,
|
||||
required String payload,
|
||||
}) async {
|
||||
const AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
|
||||
const AndroidNotificationDetails androidDetails =
|
||||
AndroidNotificationDetails(
|
||||
'chat_messages',
|
||||
'Messages de chat',
|
||||
channelDescription: 'Notifications pour les nouveaux messages de chat',
|
||||
@@ -144,21 +148,20 @@ class ChatNotificationService {
|
||||
}
|
||||
|
||||
/// Handler pour les notifications iOS reçues au premier plan
|
||||
void _onDidReceiveLocalNotification(int id, String? title, String? body, String? payload) {
|
||||
void _onDidReceiveLocalNotification(
|
||||
int id, String? title, String? body, String? payload) {
|
||||
// Traitement spécifique iOS si nécessaire
|
||||
}
|
||||
|
||||
/// Obtient et stocke le token du device
|
||||
Future<String?> _initializeDeviceToken() async {
|
||||
String? token = await _firebaseMessaging.getToken();
|
||||
if (token != null) {
|
||||
// Envoyer le token au serveur pour stocker
|
||||
await _sendTokenToServer(token);
|
||||
}
|
||||
|
||||
// Envoyer le token au serveur pour stocker
|
||||
await _sendTokenToServer(token);
|
||||
|
||||
// Écouter les changements de token
|
||||
_firebaseMessaging.onTokenRefresh.listen(_sendTokenToServer);
|
||||
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
23
app/lib/chat/services/notifications/mqtt_config.dart
Normal file → Executable file
23
app/lib/chat/services/notifications/mqtt_config.dart
Normal file → Executable file
@@ -1,6 +1,7 @@
|
||||
/// Configuration pour le broker MQTT
|
||||
///
|
||||
/// Centralise les paramètres de connexion au broker MQTT
|
||||
library;
|
||||
|
||||
class MqttConfig {
|
||||
// Configuration du serveur MQTT
|
||||
@@ -8,32 +9,32 @@ class MqttConfig {
|
||||
static const int port = 1883;
|
||||
static const int securePort = 8883;
|
||||
static const bool useSsl = false;
|
||||
|
||||
|
||||
// Configuration d'authentification
|
||||
static const String username = 'geosector_chat';
|
||||
static const String password = 'secure_password_here';
|
||||
|
||||
|
||||
// Préfixes des topics MQTT
|
||||
static const String topicBase = 'chat';
|
||||
static const String topicUserMessages = '$topicBase/user';
|
||||
static const String topicAnnouncements = '$topicBase/announcement';
|
||||
static const String topicGroups = '$topicBase/groups';
|
||||
static const String topicConversations = '$topicBase/conversation';
|
||||
|
||||
|
||||
// Configuration des sessions
|
||||
static const int keepAliveInterval = 60;
|
||||
static const int reconnectInterval = 5;
|
||||
static const bool cleanSession = true;
|
||||
|
||||
|
||||
// Configuration des notifications
|
||||
static const int notificationRetryCount = 3;
|
||||
static const Duration notificationTimeout = Duration(seconds: 30);
|
||||
|
||||
|
||||
/// Génère un client ID unique pour chaque session
|
||||
static String generateClientId(String userId) {
|
||||
return 'chat_${userId}_${DateTime.now().millisecondsSinceEpoch}';
|
||||
}
|
||||
|
||||
|
||||
/// Retourne l'URL complète du broker selon la configuration SSL
|
||||
static String get brokerUrl {
|
||||
if (useSsl) {
|
||||
@@ -42,27 +43,27 @@ class MqttConfig {
|
||||
return '$host:$port';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Retourne le topic pour les messages d'un utilisateur
|
||||
static String getUserMessageTopic(String userId) {
|
||||
return '$topicUserMessages/$userId/messages';
|
||||
}
|
||||
|
||||
|
||||
/// Retourne le topic pour les annonces globales
|
||||
static String getAnnouncementTopic() {
|
||||
return topicAnnouncements;
|
||||
}
|
||||
|
||||
|
||||
/// Retourne le topic pour une conversation spécifique
|
||||
static String getConversationTopic(String conversationId) {
|
||||
return '$topicConversations/$conversationId';
|
||||
}
|
||||
|
||||
|
||||
/// Retourne le topic pour un groupe spécifique
|
||||
static String getGroupTopic(String groupId) {
|
||||
return '$topicGroups/$groupId';
|
||||
}
|
||||
|
||||
|
||||
/// Retourne les topics auxquels un utilisateur doit s'abonner
|
||||
static List<String> getUserSubscriptionTopics(String userId) {
|
||||
return [
|
||||
|
||||
85
app/lib/chat/services/notifications/mqtt_notification_service.dart
Normal file → Executable file
85
app/lib/chat/services/notifications/mqtt_notification_service.dart
Normal file → Executable file
@@ -11,49 +11,52 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
/// et afficher des notifications locales
|
||||
|
||||
class MqttNotificationService {
|
||||
static final MqttNotificationService _instance = MqttNotificationService._internal();
|
||||
static final MqttNotificationService _instance =
|
||||
MqttNotificationService._internal();
|
||||
factory MqttNotificationService() => _instance;
|
||||
MqttNotificationService._internal();
|
||||
|
||||
late MqttServerClient _client;
|
||||
final FlutterLocalNotificationsPlugin _localNotifications = FlutterLocalNotificationsPlugin();
|
||||
|
||||
final FlutterLocalNotificationsPlugin _localNotifications =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
// Configuration
|
||||
final String mqttHost;
|
||||
final int mqttPort;
|
||||
final String mqttUsername;
|
||||
final String mqttPassword;
|
||||
final String clientId;
|
||||
|
||||
|
||||
// État
|
||||
bool _initialized = false;
|
||||
String? _userId;
|
||||
StreamSubscription? _messageSubscription;
|
||||
|
||||
|
||||
// Callbacks
|
||||
Function(String messageId)? onMessageTap;
|
||||
Function(Map<String, dynamic>)? onNotificationReceived;
|
||||
|
||||
|
||||
MqttNotificationService({
|
||||
this.mqttHost = 'mqtt.geosector.fr',
|
||||
this.mqttPort = 1883,
|
||||
this.mqttUsername = '',
|
||||
this.mqttPassword = '',
|
||||
String? clientId,
|
||||
}) : clientId = clientId ?? 'geosector_chat_${DateTime.now().millisecondsSinceEpoch}';
|
||||
}) : clientId = clientId ??
|
||||
'geosector_chat_${DateTime.now().millisecondsSinceEpoch}';
|
||||
|
||||
/// Initialise le service de notifications
|
||||
Future<void> initialize({required String userId}) async {
|
||||
if (_initialized) return;
|
||||
|
||||
|
||||
_userId = userId;
|
||||
|
||||
|
||||
// Initialiser les notifications locales
|
||||
await _initializeLocalNotifications();
|
||||
|
||||
|
||||
// Initialiser le client MQTT
|
||||
await _initializeMqttClient();
|
||||
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
@@ -61,26 +64,25 @@ class MqttNotificationService {
|
||||
Future<void> _initializeMqttClient() async {
|
||||
try {
|
||||
_client = MqttServerClient.withPort(mqttHost, clientId, mqttPort);
|
||||
|
||||
|
||||
_client.logging(on: kDebugMode);
|
||||
_client.keepAlivePeriod = 60;
|
||||
_client.onConnected = _onConnected;
|
||||
_client.onDisconnected = _onDisconnected;
|
||||
_client.onSubscribed = _onSubscribed;
|
||||
_client.autoReconnect = true;
|
||||
|
||||
|
||||
// Configurer les options de connexion
|
||||
final connMessage = MqttConnectMessage()
|
||||
.authenticateAs(mqttUsername, mqttPassword)
|
||||
.withClientIdentifier(clientId)
|
||||
.startClean()
|
||||
.keepAliveFor(60);
|
||||
|
||||
|
||||
_client.connectionMessage = connMessage;
|
||||
|
||||
|
||||
// Se connecter
|
||||
await _connect();
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de l\'initialisation MQTT : $e');
|
||||
rethrow;
|
||||
@@ -101,20 +103,20 @@ class MqttNotificationService {
|
||||
/// Callback lors de la connexion
|
||||
void _onConnected() {
|
||||
debugPrint('Connecté au broker MQTT');
|
||||
|
||||
|
||||
// S'abonner aux topics de l'utilisateur
|
||||
if (_userId != null) {
|
||||
_subscribeToUserTopics(_userId!);
|
||||
}
|
||||
|
||||
|
||||
// Écouter les messages
|
||||
_messageSubscription = _client.updates?.listen(_onMessageReceived);
|
||||
_messageSubscription = _client.updates.listen(_onMessageReceived);
|
||||
}
|
||||
|
||||
/// Callback lors de la déconnexion
|
||||
void _onDisconnected() {
|
||||
debugPrint('Déconnecté du broker MQTT');
|
||||
|
||||
|
||||
// Tenter une reconnexion
|
||||
if (_client.autoReconnect) {
|
||||
Future.delayed(const Duration(seconds: 5), () {
|
||||
@@ -132,10 +134,10 @@ class MqttNotificationService {
|
||||
void _subscribeToUserTopics(String userId) {
|
||||
// Topic pour les messages personnels
|
||||
_client.subscribe('chat/user/$userId/messages', MqttQos.atLeastOnce);
|
||||
|
||||
|
||||
// Topic pour les annonces
|
||||
_client.subscribe('chat/announcement', MqttQos.atLeastOnce);
|
||||
|
||||
|
||||
// Topic pour les groupes de l'utilisateur (si disponibles)
|
||||
_client.subscribe('chat/user/$userId/groups/+', MqttQos.atLeastOnce);
|
||||
}
|
||||
@@ -145,8 +147,9 @@ class MqttNotificationService {
|
||||
for (var message in messages) {
|
||||
final topic = message.topic;
|
||||
final payload = message.payload as MqttPublishMessage;
|
||||
final messageText = MqttUtilities.bytesToStringAsString(payload.payload.message!);
|
||||
|
||||
final messageText =
|
||||
MqttUtilities.bytesToStringAsString(payload.payload.message!);
|
||||
|
||||
try {
|
||||
final data = jsonDecode(messageText) as Map<String, dynamic>;
|
||||
_handleNotification(topic, data);
|
||||
@@ -157,17 +160,18 @@ class MqttNotificationService {
|
||||
}
|
||||
|
||||
/// Traite la notification reçue
|
||||
Future<void> _handleNotification(String topic, Map<String, dynamic> data) async {
|
||||
Future<void> _handleNotification(
|
||||
String topic, Map<String, dynamic> data) async {
|
||||
// Vérifier les paramètres de notification de l'utilisateur
|
||||
if (!await _shouldShowNotification(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
String title = '';
|
||||
String body = '';
|
||||
String messageId = '';
|
||||
String conversationId = '';
|
||||
|
||||
|
||||
if (topic.startsWith('chat/user/')) {
|
||||
// Message personnel
|
||||
title = data['senderName'] ?? 'Nouveau message';
|
||||
@@ -181,7 +185,7 @@ class MqttNotificationService {
|
||||
messageId = data['messageId'] ?? '';
|
||||
conversationId = data['conversationId'] ?? '';
|
||||
}
|
||||
|
||||
|
||||
// Afficher la notification locale
|
||||
await _showLocalNotification(
|
||||
title: title,
|
||||
@@ -191,7 +195,7 @@ class MqttNotificationService {
|
||||
'conversationId': conversationId,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
// Appeler le callback si défini
|
||||
onNotificationReceived?.call(data);
|
||||
}
|
||||
@@ -207,18 +211,19 @@ class MqttNotificationService {
|
||||
|
||||
/// Initialise les notifications locales
|
||||
Future<void> _initializeLocalNotifications() async {
|
||||
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const androidSettings =
|
||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const iosSettings = DarwinInitializationSettings(
|
||||
requestAlertPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestSoundPermission: true,
|
||||
);
|
||||
|
||||
|
||||
const initSettings = InitializationSettings(
|
||||
android: androidSettings,
|
||||
iOS: iosSettings,
|
||||
);
|
||||
|
||||
|
||||
await _localNotifications.initialize(
|
||||
initSettings,
|
||||
onDidReceiveNotificationResponse: _onNotificationTap,
|
||||
@@ -239,18 +244,18 @@ class MqttNotificationService {
|
||||
priority: Priority.high,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
);
|
||||
|
||||
|
||||
const iosDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
);
|
||||
|
||||
|
||||
const notificationDetails = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iosDetails,
|
||||
);
|
||||
|
||||
|
||||
await _localNotifications.show(
|
||||
DateTime.now().microsecondsSinceEpoch,
|
||||
title,
|
||||
@@ -277,22 +282,24 @@ class MqttNotificationService {
|
||||
}
|
||||
|
||||
/// Publie un message MQTT
|
||||
Future<void> publishMessage(String topic, Map<String, dynamic> message) async {
|
||||
Future<void> publishMessage(
|
||||
String topic, Map<String, dynamic> message) async {
|
||||
if (_client.connectionStatus?.state != MqttConnectionState.connected) {
|
||||
await _connect();
|
||||
}
|
||||
|
||||
|
||||
final messagePayload = jsonEncode(message);
|
||||
final builder = MqttPayloadBuilder();
|
||||
builder.addString(messagePayload);
|
||||
|
||||
|
||||
_client.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!);
|
||||
}
|
||||
|
||||
/// S'abonner à une conversation spécifique
|
||||
Future<void> subscribeToConversation(String conversationId) async {
|
||||
if (_client.connectionStatus?.state == MqttConnectionState.connected) {
|
||||
_client.subscribe('chat/conversation/$conversationId', MqttQos.atLeastOnce);
|
||||
_client.subscribe(
|
||||
'chat/conversation/$conversationId', MqttQos.atLeastOnce);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
4
app/lib/chat/services/offline_queue_service.dart
Normal file → Executable file
4
app/lib/chat/services/offline_queue_service.dart
Normal file → Executable file
@@ -2,6 +2,7 @@
|
||||
///
|
||||
/// Ce service gère les opérations chat en mode hors ligne
|
||||
/// et les synchronise lorsque la connexion revient
|
||||
library;
|
||||
|
||||
class OfflineQueueService {
|
||||
// TODO: Ajouter le service de connectivité
|
||||
@@ -9,7 +10,8 @@ class OfflineQueueService {
|
||||
OfflineQueueService();
|
||||
|
||||
/// Ajoute une opération en attente
|
||||
Future<void> addPendingOperation(String operationType, Map<String, dynamic> data) async {
|
||||
Future<void> addPendingOperation(
|
||||
String operationType, Map<String, dynamic> data) async {
|
||||
// TODO: Implémenter l'ajout à la file d'attente
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
0
app/lib/chat/widgets/chat_input.dart
Normal file → Executable file
0
app/lib/chat/widgets/chat_input.dart
Normal file → Executable file
0
app/lib/chat/widgets/chat_screen.dart
Normal file → Executable file
0
app/lib/chat/widgets/chat_screen.dart
Normal file → Executable file
0
app/lib/chat/widgets/conversations_list.dart
Normal file → Executable file
0
app/lib/chat/widgets/conversations_list.dart
Normal file → Executable file
15
app/lib/chat/widgets/message_bubble.dart
Normal file → Executable file
15
app/lib/chat/widgets/message_bubble.dart
Normal file → Executable file
@@ -30,32 +30,35 @@ class MessageBubble extends StatelessWidget {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showSenderInfo) CircleAvatar(child: Text('S')),
|
||||
if (showSenderInfo) const CircleAvatar(child: Text('S')),
|
||||
Expanded(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
margin: const EdgeInsets.only(left: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isAnnouncement ? Colors.orange.shade100 : Colors.blue.shade100,
|
||||
color: isAnnouncement
|
||||
? Colors.orange.shade100
|
||||
: Colors.blue.shade100,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showSenderInfo)
|
||||
Text(
|
||||
const Text(
|
||||
'Expéditeur',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text('Contenu du message...'),
|
||||
const Text('Contenu du message...'),
|
||||
if (showTimestamp || showStatus)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (showTimestamp) Text('12:34', style: TextStyle(fontSize: 12)),
|
||||
if (showTimestamp)
|
||||
const Text('12:34', style: TextStyle(fontSize: 12)),
|
||||
if (showStatus) const SizedBox(width: 4),
|
||||
if (showStatus) Icon(Icons.check, size: 16),
|
||||
if (showStatus) const Icon(Icons.check, size: 16),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
0
app/lib/chat/widgets/notification_settings_widget.dart
Normal file → Executable file
0
app/lib/chat/widgets/notification_settings_widget.dart
Normal file → Executable file
19
app/lib/core/constants/app_keys.dart
Normal file → Executable file
19
app/lib/core/constants/app_keys.dart
Normal file → Executable file
@@ -50,9 +50,12 @@ class AppKeys {
|
||||
static const Duration sessionDefaultExpiry = Duration(days: 7);
|
||||
|
||||
// Clés API externes
|
||||
static const String mapboxApiKeyDev = 'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY21hanVmNjN5MTM5djJtczdsMW92cjQ0ciJ9.pUCMuvWPB3cuBaPh4ywTAw';
|
||||
static const String mapboxApiKeyRec = 'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY21hanVlZ3FiMGx0NDJpc2k4YnkxaWZ2dSJ9.OqGJtjlWRgB4fIjECCB8WA';
|
||||
static const String mapboxApiKeyProd = 'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY204dTNhNmd0MGV1ZzJqc2pnNnB0NjYxdSJ9.TA5Mvliyn91Oi01F_2Yuxw';
|
||||
static const String mapboxApiKeyDev =
|
||||
'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY21hanVmNjN5MTM5djJtczdsMW92cjQ0ciJ9.pUCMuvWPB3cuBaPh4ywTAw';
|
||||
static const String mapboxApiKeyRec =
|
||||
'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY21hanVlZ3FiMGx0NDJpc2k4YnkxaWZ2dSJ9.OqGJtjlWRgB4fIjECCB8WA';
|
||||
static const String mapboxApiKeyProd =
|
||||
'pk.eyJ1IjoicHZkNnNvZnQiLCJhIjoiY204dTNhNmd0MGV1ZzJqc2pnNnB0NjYxdSJ9.TA5Mvliyn91Oi01F_2Yuxw';
|
||||
|
||||
// Méthode pour obtenir la clé API Mapbox en fonction de l'environnement actuel
|
||||
static String getMapboxApiKey(String environment) {
|
||||
@@ -114,11 +117,6 @@ class AppKeys {
|
||||
|
||||
// Types de règlements (basés sur la maquette Figma)
|
||||
static const Map<int, Map<String, dynamic>> typesReglements = {
|
||||
0: {
|
||||
'titre': 'Pas de règlement',
|
||||
'couleur': 0xFF757575, // Gris foncé
|
||||
'icon_data': Icons.money_off,
|
||||
},
|
||||
1: {
|
||||
'titre': 'Espèce',
|
||||
'couleur': 0xFFDAA520, // Goldenrod
|
||||
@@ -134,6 +132,11 @@ class AppKeys {
|
||||
'couleur': 0xFF0099FF, // Bleu flashy
|
||||
'icon_data': Icons.credit_card,
|
||||
},
|
||||
4: {
|
||||
'titre': 'Non renseigné',
|
||||
'couleur': 0xFF9E9E9E, // Gris moyen
|
||||
'icon_data': Icons.help_outline,
|
||||
},
|
||||
};
|
||||
|
||||
// Types de passages (basés sur la maquette Figma)
|
||||
|
||||
0
app/lib/core/constants/reponse-login.json
Normal file → Executable file
0
app/lib/core/constants/reponse-login.json
Normal file → Executable file
2
app/lib/core/data/models/amicale_model.dart
Normal file → Executable file
2
app/lib/core/data/models/amicale_model.dart
Normal file → Executable file
@@ -222,7 +222,7 @@ class AmicaleModel extends HiveObject {
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return AmicaleModel(
|
||||
id: this.id,
|
||||
id: id,
|
||||
name: name ?? this.name,
|
||||
adresse1: adresse1 ?? this.adresse1,
|
||||
adresse2: adresse2 ?? this.adresse2,
|
||||
|
||||
0
app/lib/core/data/models/client_model.dart
Normal file → Executable file
0
app/lib/core/data/models/client_model.dart
Normal file → Executable file
16
app/lib/core/data/models/membre_model.dart
Normal file → Executable file
16
app/lib/core/data/models/membre_model.dart
Normal file → Executable file
@@ -4,6 +4,22 @@ import 'package:geosector_app/core/data/models/user_model.dart';
|
||||
|
||||
part 'membre_model.g.dart';
|
||||
|
||||
/// Modèle représentant un membre d'une amicale.
|
||||
///
|
||||
/// IMPORTANT : Ce modèle représente TOUS les membres d'une amicale,
|
||||
/// pas seulement l'utilisateur connecté. Pour l'utilisateur connecté, voir UserModel.
|
||||
///
|
||||
/// La box Hive 'membres' contient plusieurs enregistrements (tous les membres de l'amicale).
|
||||
///
|
||||
/// Relations avec les autres modèles :
|
||||
/// - UserModel : représente uniquement l'utilisateur connecté (current user)
|
||||
/// - UserSectorModel : utilise MembreModel.id pour associer les membres aux secteurs
|
||||
/// ATTENTION : UserSectorModel.id = MembreModel.id (pas UserModel.id)
|
||||
///
|
||||
/// Chaque membre a son propre ID unique qui est utilisé pour :
|
||||
/// - L'attribution aux secteurs (via UserSectorModel)
|
||||
/// - La gestion des passages
|
||||
/// - Les statistiques par membre
|
||||
@HiveType(typeId: 5) // Utilisation d'un typeId unique
|
||||
class MembreModel extends HiveObject {
|
||||
@HiveField(0)
|
||||
|
||||
0
app/lib/core/data/models/operation_model.dart
Normal file → Executable file
0
app/lib/core/data/models/operation_model.dart
Normal file → Executable file
24
app/lib/core/data/models/passage_model.dart
Normal file → Executable file
24
app/lib/core/data/models/passage_model.dart
Normal file → Executable file
@@ -12,7 +12,7 @@ class PassageModel extends HiveObject {
|
||||
final int fkOperation;
|
||||
|
||||
@HiveField(2)
|
||||
final int fkSector;
|
||||
final int? fkSector;
|
||||
|
||||
@HiveField(3)
|
||||
final int fkUser;
|
||||
@@ -24,7 +24,7 @@ class PassageModel extends HiveObject {
|
||||
final String fkAdresse;
|
||||
|
||||
@HiveField(6)
|
||||
final DateTime passedAt;
|
||||
final DateTime? passedAt;
|
||||
|
||||
@HiveField(7)
|
||||
final String numero;
|
||||
@@ -95,11 +95,11 @@ class PassageModel extends HiveObject {
|
||||
PassageModel({
|
||||
required this.id,
|
||||
required this.fkOperation,
|
||||
required this.fkSector,
|
||||
this.fkSector,
|
||||
required this.fkUser,
|
||||
required this.fkType,
|
||||
required this.fkAdresse,
|
||||
required this.passedAt,
|
||||
this.passedAt,
|
||||
required this.numero,
|
||||
required this.rue,
|
||||
this.rueBis = '',
|
||||
@@ -136,7 +136,11 @@ class PassageModel extends HiveObject {
|
||||
final int fkOperation = rawFkOperation is String ? int.parse(rawFkOperation) : rawFkOperation as int;
|
||||
|
||||
final dynamic rawFkSector = json['fk_sector'];
|
||||
final int fkSector = rawFkSector is String ? int.parse(rawFkSector) : rawFkSector as int;
|
||||
final int? fkSector = rawFkSector == null
|
||||
? null
|
||||
: rawFkSector is String
|
||||
? int.parse(rawFkSector)
|
||||
: rawFkSector as int;
|
||||
|
||||
final dynamic rawFkUser = json['fk_user'];
|
||||
final int fkUser = rawFkUser is String ? int.parse(rawFkUser) : rawFkUser as int;
|
||||
@@ -153,8 +157,10 @@ class PassageModel extends HiveObject {
|
||||
final dynamic rawNbPassages = json['nb_passages'];
|
||||
final int nbPassages = rawNbPassages is String ? int.parse(rawNbPassages) : rawNbPassages as int;
|
||||
|
||||
// Convertir la date
|
||||
final DateTime passedAt = DateTime.parse(json['passed_at']);
|
||||
// Convertir la date (nullable)
|
||||
final DateTime? passedAt = json['passed_at'] != null
|
||||
? DateTime.parse(json['passed_at'])
|
||||
: null;
|
||||
|
||||
return PassageModel(
|
||||
id: id,
|
||||
@@ -203,7 +209,7 @@ class PassageModel extends HiveObject {
|
||||
'fk_user': fkUser,
|
||||
'fk_type': fkType,
|
||||
'fk_adresse': fkAdresse,
|
||||
'passed_at': passedAt.toIso8601String(),
|
||||
'passed_at': passedAt?.toIso8601String(),
|
||||
'numero': numero,
|
||||
'rue': rue,
|
||||
'rue_bis': rueBis,
|
||||
@@ -293,6 +299,6 @@ class PassageModel extends HiveObject {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PassageModel(id: $id, fkOperation: $fkOperation, fkSector: $fkSector, fkUser: $fkUser, fkType: $fkType, adresse: $fkAdresse, ville: $ville, montant: $montant)';
|
||||
return 'PassageModel(id: $id, fkOperation: $fkOperation, fkSector: $fkSector, fkUser: $fkUser, fkType: $fkType, adresse: $fkAdresse, ville: $ville, montant: $montant, passedAt: $passedAt)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ class PassageModelAdapter extends TypeAdapter<PassageModel> {
|
||||
return PassageModel(
|
||||
id: fields[0] as int,
|
||||
fkOperation: fields[1] as int,
|
||||
fkSector: fields[2] as int,
|
||||
fkSector: fields[2] as int?,
|
||||
fkUser: fields[3] as int,
|
||||
fkType: fields[4] as int,
|
||||
fkAdresse: fields[5] as String,
|
||||
passedAt: fields[6] as DateTime,
|
||||
passedAt: fields[6] as DateTime?,
|
||||
numero: fields[7] as String,
|
||||
rue: fields[8] as String,
|
||||
rueBis: fields[9] as String,
|
||||
|
||||
0
app/lib/core/data/models/region_model.dart
Normal file → Executable file
0
app/lib/core/data/models/region_model.dart
Normal file → Executable file
0
app/lib/core/data/models/sector_model.dart
Normal file → Executable file
0
app/lib/core/data/models/sector_model.dart
Normal file → Executable file
14
app/lib/core/data/models/user_model.dart
Normal file → Executable file
14
app/lib/core/data/models/user_model.dart
Normal file → Executable file
@@ -2,6 +2,16 @@ import 'package:hive/hive.dart';
|
||||
|
||||
part 'user_model.g.dart';
|
||||
|
||||
/// Modèle représentant l'utilisateur actuellement connecté (current user).
|
||||
///
|
||||
/// IMPORTANT : Ce modèle est utilisé UNIQUEMENT pour l'utilisateur connecté,
|
||||
/// pas pour les membres de l'amicale. Pour les membres, utilisez MembreModel.
|
||||
///
|
||||
/// La box Hive 'users' ne contient qu'un seul enregistrement : l'utilisateur actuel.
|
||||
///
|
||||
/// Relations avec les autres modèles :
|
||||
/// - MembreModel : représente TOUS les membres d'une amicale (y compris l'utilisateur actuel s'il est membre)
|
||||
/// - UserSectorModel : associe les membres (pas les users) aux secteurs
|
||||
@HiveType(typeId: 0)
|
||||
class UserModel extends HiveObject {
|
||||
@HiveField(0)
|
||||
@@ -201,13 +211,13 @@ class UserModel extends HiveObject {
|
||||
DateTime? dateEmbauche,
|
||||
}) {
|
||||
return UserModel(
|
||||
id: this.id,
|
||||
id: id,
|
||||
email: email ?? this.email,
|
||||
name: name ?? this.name,
|
||||
username: username ?? this.username,
|
||||
firstName: firstName ?? this.firstName,
|
||||
role: role ?? this.role,
|
||||
createdAt: this.createdAt,
|
||||
createdAt: createdAt,
|
||||
lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt,
|
||||
isActive: isActive ?? this.isActive,
|
||||
isSynced: isSynced ?? this.isSynced,
|
||||
|
||||
0
app/lib/core/data/models/user_sector_model.dart
Normal file → Executable file
0
app/lib/core/data/models/user_sector_model.dart
Normal file → Executable file
0
app/lib/core/models/loading_state.dart
Normal file → Executable file
0
app/lib/core/models/loading_state.dart
Normal file → Executable file
0
app/lib/core/repositories/amicale_repository.dart
Normal file → Executable file
0
app/lib/core/repositories/amicale_repository.dart
Normal file → Executable file
0
app/lib/core/repositories/client_repository.dart
Normal file → Executable file
0
app/lib/core/repositories/client_repository.dart
Normal file → Executable file
55
app/lib/core/repositories/membre_repository.dart
Normal file → Executable file
55
app/lib/core/repositories/membre_repository.dart
Normal file → Executable file
@@ -9,15 +9,28 @@ class MembreRepository extends ChangeNotifier {
|
||||
// Constructeur sans paramètres - utilise ApiService.instance
|
||||
MembreRepository();
|
||||
|
||||
// Cache de la box pour éviter les vérifications répétées
|
||||
Box<MembreModel>? _cachedMembreBox;
|
||||
|
||||
// Utiliser un getter lazy pour n'accéder à la boîte que lorsque nécessaire
|
||||
// et vérifier qu'elle est ouverte avant accès
|
||||
Box<MembreModel> get _membreBox {
|
||||
_ensureBoxIsOpen();
|
||||
return Hive.box<MembreModel>(AppKeys.membresBoxName);
|
||||
if (_cachedMembreBox == null) {
|
||||
if (!Hive.isBoxOpen(AppKeys.membresBoxName)) {
|
||||
throw Exception('La boîte ${AppKeys.membresBoxName} n\'est pas ouverte. Initialisez d\'abord l\'application.');
|
||||
}
|
||||
_cachedMembreBox = Hive.box<MembreModel>(AppKeys.membresBoxName);
|
||||
debugPrint('MembreRepository: Box ${AppKeys.membresBoxName} mise en cache');
|
||||
}
|
||||
return _cachedMembreBox!;
|
||||
}
|
||||
|
||||
bool _isLoading = false;
|
||||
|
||||
// Méthode pour réinitialiser le cache après modification de la box
|
||||
void _resetCache() {
|
||||
_cachedMembreBox = null;
|
||||
}
|
||||
|
||||
// Getters
|
||||
bool get isLoading => _isLoading;
|
||||
List<MembreModel> get membres => getAllMembres();
|
||||
@@ -35,14 +48,6 @@ class MembreRepository extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// Méthode pour vérifier si la boîte est ouverte et l'ouvrir si nécessaire
|
||||
Future<void> _ensureBoxIsOpen() async {
|
||||
const boxName = AppKeys.membresBoxName;
|
||||
if (!Hive.isBoxOpen(boxName)) {
|
||||
debugPrint('Ouverture de la boîte $boxName dans MembreRepository...');
|
||||
await Hive.openBox<MembreModel>(boxName);
|
||||
}
|
||||
}
|
||||
|
||||
// === MÉTHODES SPÉCIFIQUES AUX MEMBRES ===
|
||||
|
||||
@@ -101,12 +106,14 @@ class MembreRepository extends ChangeNotifier {
|
||||
// Sauvegarder un membre
|
||||
Future<void> saveMembreBox(MembreModel membre) async {
|
||||
await _membreBox.put(membre.id, membre);
|
||||
_resetCache(); // Réinitialiser le cache après modification
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Supprimer un membre
|
||||
Future<void> deleteMembreBox(int id) async {
|
||||
await _membreBox.delete(id);
|
||||
_resetCache(); // Réinitialiser le cache après modification
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -154,7 +161,7 @@ class MembreRepository extends ChangeNotifier {
|
||||
isActive: membre.isActive,
|
||||
);
|
||||
|
||||
// Sauvegarder localement dans Hive
|
||||
// Sauvegarder localement dans Hive (saveMembreBox gère déjà _resetCache)
|
||||
await saveMembreBox(createdMember);
|
||||
|
||||
debugPrint('✅ Membre créé avec l\'ID: $userId et sauvegardé localement');
|
||||
@@ -200,6 +207,28 @@ class MembreRepository extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// Réinitialiser le mot de passe d'un membre via l'API
|
||||
Future<bool> resetMemberPassword(int membreId) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await ApiService.instance.post('/users/$membreId/reset-password');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la réinitialisation du mot de passe: $e');
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer un membre via l'API avec transfert optionnel
|
||||
Future<bool> deleteMembre(int membreId, [int? transferToUserId, int? operationId]) async {
|
||||
_isLoading = true;
|
||||
@@ -283,6 +312,7 @@ class MembreRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
debugPrint('$count membres traités et stockés');
|
||||
_resetCache(); // Réinitialiser le cache après traitement des données API
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du traitement des membres: $e');
|
||||
@@ -337,6 +367,7 @@ class MembreRepository extends ChangeNotifier {
|
||||
// Vider la boîte des membres
|
||||
Future<void> clearMembres() async {
|
||||
await _membreBox.clear();
|
||||
_resetCache(); // Réinitialiser le cache après suppression de toutes les données
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
122
app/lib/core/repositories/operation_repository.dart
Normal file → Executable file
122
app/lib/core/repositories/operation_repository.dart
Normal file → Executable file
@@ -51,7 +51,9 @@ class OperationRepository extends ChangeNotifier {
|
||||
OperationModel? getCurrentOperation() {
|
||||
try {
|
||||
// Récupérer toutes les opérations actives
|
||||
final activeOperations = _operationBox.values.where((operation) => operation.isActive == true).toList();
|
||||
final activeOperations = _operationBox.values
|
||||
.where((operation) => operation.isActive == true)
|
||||
.toList();
|
||||
|
||||
if (activeOperations.isEmpty) {
|
||||
debugPrint('⚠️ Aucune opération active trouvée');
|
||||
@@ -62,10 +64,12 @@ class OperationRepository extends ChangeNotifier {
|
||||
activeOperations.sort((a, b) => b.id.compareTo(a.id));
|
||||
final currentOperation = activeOperations.first;
|
||||
|
||||
debugPrint('🎯 Opération courante: ${currentOperation.id} - ${currentOperation.name}');
|
||||
debugPrint(
|
||||
'🎯 Opération courante: ${currentOperation.id} - ${currentOperation.name}');
|
||||
return currentOperation;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors de la récupération de l\'opération courante: $e');
|
||||
debugPrint(
|
||||
'❌ Erreur lors de la récupération de l\'opération courante: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -79,7 +83,10 @@ class OperationRepository extends ChangeNotifier {
|
||||
// Méthode pour récupérer toutes les opérations actives (utile pour debug/admin)
|
||||
List<OperationModel> getActiveOperations() {
|
||||
try {
|
||||
return _operationBox.values.where((operation) => operation.isActive == true).toList()..sort((a, b) => b.id.compareTo(a.id)); // Tri par ID décroissant
|
||||
return _operationBox.values
|
||||
.where((operation) => operation.isActive == true)
|
||||
.toList()
|
||||
..sort((a, b) => b.id.compareTo(a.id)); // Tri par ID décroissant
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors de la récupération des opérations actives: $e');
|
||||
return [];
|
||||
@@ -104,13 +111,17 @@ class OperationRepository extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
debugPrint('🔄 Traitement de ${operationsData.length} opérations depuis l\'API');
|
||||
debugPrint(
|
||||
'🔄 Traitement de ${operationsData.length} opérations depuis l\'API');
|
||||
|
||||
for (var operationData in operationsData) {
|
||||
final operationJson = operationData as Map<String, dynamic>;
|
||||
final operationId = operationJson['id'] is String ? int.parse(operationJson['id']) : operationJson['id'] as int;
|
||||
final operationId = operationJson['id'] is String
|
||||
? int.parse(operationJson['id'])
|
||||
: operationJson['id'] as int;
|
||||
|
||||
debugPrint('📝 Traitement opération ID: $operationId, libelle: ${operationJson['libelle']}');
|
||||
debugPrint(
|
||||
'📝 Traitement opération ID: $operationId, libelle: ${operationJson['libelle']}');
|
||||
|
||||
// Vérifier si l'opération existe déjà
|
||||
OperationModel? existingOperation = getOperationById(operationId);
|
||||
@@ -123,11 +134,14 @@ class OperationRepository extends ChangeNotifier {
|
||||
} else {
|
||||
// Mettre à jour l'opération existante
|
||||
final updatedOperation = existingOperation.copyWith(
|
||||
name: operationJson['libelle'], // ← Correction: utiliser 'libelle' au lieu de 'name'
|
||||
name: operationJson[
|
||||
'libelle'], // ← Correction: utiliser 'libelle' au lieu de 'name'
|
||||
fkEntite: operationJson['fk_entite'],
|
||||
dateDebut: DateTime.parse(operationJson['date_deb']),
|
||||
dateFin: DateTime.parse(operationJson['date_fin']),
|
||||
isActive: operationJson['chk_active'] == true || operationJson['chk_active'] == 1 || operationJson['chk_active'] == "1",
|
||||
isActive: operationJson['chk_active'] == true ||
|
||||
operationJson['chk_active'] == 1 ||
|
||||
operationJson['chk_active'] == "1",
|
||||
lastSyncedAt: DateTime.now(),
|
||||
isSynced: true,
|
||||
);
|
||||
@@ -136,7 +150,8 @@ class OperationRepository extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('🎉 Traitement terminé - ${_operationBox.length} opérations dans la box');
|
||||
debugPrint(
|
||||
'🎉 Traitement terminé - ${_operationBox.length} opérations dans la box');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors du traitement des opérations: $e');
|
||||
debugPrint('❌ Stack trace: ${StackTrace.current}');
|
||||
@@ -147,7 +162,8 @@ class OperationRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Créer une opération
|
||||
Future<bool> createOperation(String name, DateTime dateDebut, DateTime dateFin) async {
|
||||
Future<bool> createOperation(
|
||||
String name, DateTime dateDebut, DateTime dateFin) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
@@ -155,14 +171,17 @@ class OperationRepository extends ChangeNotifier {
|
||||
// Préparer les données pour l'API
|
||||
final Map<String, dynamic> data = {
|
||||
'name': name,
|
||||
'date_deb': dateDebut.toIso8601String().split('T')[0], // Format YYYY-MM-DD
|
||||
'date_fin': dateFin.toIso8601String().split('T')[0], // Format YYYY-MM-DD
|
||||
'date_deb':
|
||||
dateDebut.toIso8601String().split('T')[0], // Format YYYY-MM-DD
|
||||
'date_fin':
|
||||
dateFin.toIso8601String().split('T')[0], // Format YYYY-MM-DD
|
||||
};
|
||||
|
||||
debugPrint('🚀 Création d\'une nouvelle opération: $data');
|
||||
|
||||
// Appeler l'API pour créer l'opération
|
||||
final response = await ApiService.instance.post('/operations', data: data);
|
||||
final response =
|
||||
await ApiService.instance.post('/operations', data: data);
|
||||
|
||||
if (response.statusCode == 201 || response.statusCode == 200) {
|
||||
debugPrint('✅ Opération créée avec succès');
|
||||
@@ -184,7 +203,8 @@ class OperationRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Traiter la réponse complète après création d'opération
|
||||
Future<void> _processCreationResponse(Map<String, dynamic> responseData) async {
|
||||
Future<void> _processCreationResponse(
|
||||
Map<String, dynamic> responseData) async {
|
||||
try {
|
||||
debugPrint('🔄 Traitement de la réponse de création d\'opération');
|
||||
|
||||
@@ -196,19 +216,22 @@ class OperationRepository extends ChangeNotifier {
|
||||
|
||||
// Traiter les secteurs (groupe secteurs) via DataLoadingService
|
||||
if (responseData['secteurs'] != null) {
|
||||
await DataLoadingService.instance.processSectorsFromApi(responseData['secteurs']);
|
||||
await DataLoadingService.instance
|
||||
.processSectorsFromApi(responseData['secteurs']);
|
||||
debugPrint('✅ Secteurs traités');
|
||||
}
|
||||
|
||||
// Traiter les passages (groupe passages) via DataLoadingService
|
||||
if (responseData['passages'] != null) {
|
||||
await DataLoadingService.instance.processPassagesFromApi(responseData['passages']);
|
||||
await DataLoadingService.instance
|
||||
.processPassagesFromApi(responseData['passages']);
|
||||
debugPrint('✅ Passages traités');
|
||||
}
|
||||
|
||||
// Traiter les users_sectors (groupe users_sectors) via DataLoadingService
|
||||
if (responseData['users_sectors'] != null) {
|
||||
await DataLoadingService.instance.processUserSectorsFromApi(responseData['users_sectors']);
|
||||
await DataLoadingService.instance
|
||||
.processUserSectorsFromApi(responseData['users_sectors']);
|
||||
debugPrint('✅ Users_sectors traités');
|
||||
}
|
||||
|
||||
@@ -268,7 +291,12 @@ class OperationRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Mettre à jour une opération
|
||||
Future<bool> updateOperation(int id, {String? name, DateTime? dateDebut, DateTime? dateFin, bool? isActive, int? fkEntite}) async {
|
||||
Future<bool> updateOperation(int id,
|
||||
{String? name,
|
||||
DateTime? dateDebut,
|
||||
DateTime? dateFin,
|
||||
bool? isActive,
|
||||
int? fkEntite}) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
@@ -284,15 +312,22 @@ class OperationRepository extends ChangeNotifier {
|
||||
final Map<String, dynamic> data = {
|
||||
'id': id,
|
||||
'name': name ?? existingOperation.name,
|
||||
'date_deb': (dateDebut ?? existingOperation.dateDebut).toIso8601String().split('T')[0],
|
||||
'date_fin': (dateFin ?? existingOperation.dateFin).toIso8601String().split('T')[0],
|
||||
'chk_active': isActive ?? existingOperation.isActive, // Utiliser chk_active comme dans l'API
|
||||
'fk_entite': fkEntite ?? existingOperation.fkEntite, // ← Inclure fkEntite
|
||||
'date_deb': (dateDebut ?? existingOperation.dateDebut)
|
||||
.toIso8601String()
|
||||
.split('T')[0],
|
||||
'date_fin': (dateFin ?? existingOperation.dateFin)
|
||||
.toIso8601String()
|
||||
.split('T')[0],
|
||||
'chk_active': isActive ??
|
||||
existingOperation.isActive, // Utiliser chk_active comme dans l'API
|
||||
'fk_entite':
|
||||
fkEntite ?? existingOperation.fkEntite, // ← Inclure fkEntite
|
||||
};
|
||||
|
||||
debugPrint('🔄 Mise à jour de l\'opération $id avec les données: $data');
|
||||
// Appeler l'API pour mettre à jour l'opération
|
||||
final response = await ApiService.instance.put('/operations/$id', data: data);
|
||||
final response =
|
||||
await ApiService.instance.put('/operations/$id', data: data);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
debugPrint('✅ Opération $id mise à jour avec succès');
|
||||
@@ -375,7 +410,8 @@ class OperationRepository extends ChangeNotifier {
|
||||
final response = await ApiService.instance.delete('/operations/$id');
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 204) {
|
||||
debugPrint('✅ Suppression opération active réussie - Traitement complet');
|
||||
debugPrint(
|
||||
'✅ Suppression opération active réussie - Traitement complet');
|
||||
|
||||
// Traiter la réponse complète qui contient tous les groupes de données
|
||||
if (response.data != null) {
|
||||
@@ -389,7 +425,8 @@ class OperationRepository extends ChangeNotifier {
|
||||
return true;
|
||||
}
|
||||
|
||||
debugPrint('❌ Échec suppression opération active - Code: ${response.statusCode}');
|
||||
debugPrint(
|
||||
'❌ Échec suppression opération active - Code: ${response.statusCode}');
|
||||
return false;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors de la suppression de l\'opération active: $e');
|
||||
@@ -401,9 +438,11 @@ class OperationRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Traiter la réponse complète après suppression d'opération active
|
||||
Future<void> _processActiveDeleteResponse(Map<String, dynamic> responseData) async {
|
||||
Future<void> _processActiveDeleteResponse(
|
||||
Map<String, dynamic> responseData) async {
|
||||
try {
|
||||
debugPrint('🔄 Traitement de la réponse de suppression d\'opération active');
|
||||
debugPrint(
|
||||
'🔄 Traitement de la réponse de suppression d\'opération active');
|
||||
|
||||
// Vider toutes les Box concernées
|
||||
await _clearAllRelatedBoxes();
|
||||
@@ -416,25 +455,30 @@ class OperationRepository extends ChangeNotifier {
|
||||
|
||||
// Traiter les secteurs (groupe secteurs) via DataLoadingService
|
||||
if (responseData['secteurs'] != null) {
|
||||
await DataLoadingService.instance.processSectorsFromApi(responseData['secteurs']);
|
||||
await DataLoadingService.instance
|
||||
.processSectorsFromApi(responseData['secteurs']);
|
||||
debugPrint('✅ Secteurs traités');
|
||||
}
|
||||
|
||||
// Traiter les passages (groupe passages) via DataLoadingService
|
||||
if (responseData['passages'] != null) {
|
||||
await DataLoadingService.instance.processPassagesFromApi(responseData['passages']);
|
||||
await DataLoadingService.instance
|
||||
.processPassagesFromApi(responseData['passages']);
|
||||
debugPrint('✅ Passages traités');
|
||||
}
|
||||
|
||||
// Traiter les users_sectors (groupe users_sectors) via DataLoadingService
|
||||
if (responseData['users_sectors'] != null) {
|
||||
await DataLoadingService.instance.processUserSectorsFromApi(responseData['users_sectors']);
|
||||
await DataLoadingService.instance
|
||||
.processUserSectorsFromApi(responseData['users_sectors']);
|
||||
debugPrint('✅ Users_sectors traités');
|
||||
}
|
||||
|
||||
debugPrint('🎉 Tous les groupes de données ont été traités après suppression opération active');
|
||||
debugPrint(
|
||||
'🎉 Tous les groupes de données ont été traités après suppression opération active');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors du traitement de la réponse de suppression: $e');
|
||||
debugPrint(
|
||||
'❌ Erreur lors du traitement de la réponse de suppression: $e');
|
||||
// Ne pas faire échouer la suppression si le traitement des données supplémentaires échoue
|
||||
}
|
||||
}
|
||||
@@ -456,7 +500,8 @@ class OperationRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
if (Hive.isBoxOpen(AppKeys.userSectorBoxName)) {
|
||||
final userSectorsBox = Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
|
||||
final userSectorsBox =
|
||||
Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
|
||||
await userSectorsBox.clear();
|
||||
}
|
||||
|
||||
@@ -467,14 +512,17 @@ class OperationRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Export Excel d'une opération
|
||||
Future<void> exportOperationToExcel(int operationId, String operationName) async {
|
||||
Future<void> exportOperationToExcel(
|
||||
int operationId, String operationName) async {
|
||||
try {
|
||||
debugPrint('📊 Export Excel opération $operationId: $operationName');
|
||||
|
||||
// Générer le nom de fichier avec la date actuelle
|
||||
final now = DateTime.now();
|
||||
final dateStr = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
|
||||
final fileName = 'operation_${operationName.replaceAll(' ', '_')}_$dateStr.xlsx';
|
||||
final dateStr =
|
||||
'${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
|
||||
final fileName =
|
||||
'operation_${operationName.replaceAll(' ', '_')}_$dateStr.xlsx';
|
||||
|
||||
// Appeler l'API pour télécharger le fichier Excel
|
||||
await ApiService.instance.downloadOperationExcel(operationId, fileName);
|
||||
|
||||
59
app/lib/core/repositories/passage_repository.dart
Normal file → Executable file
59
app/lib/core/repositories/passage_repository.dart
Normal file → Executable file
@@ -12,11 +12,24 @@ class PassageRepository extends ChangeNotifier {
|
||||
// Cache pour les statistiques
|
||||
Map<String, dynamic>? _cachedStats;
|
||||
|
||||
// Cache de la box pour éviter les vérifications répétées
|
||||
Box<PassageModel>? _cachedPassageBox;
|
||||
|
||||
// Utiliser un getter lazy pour n'accéder à la boîte que lorsque nécessaire
|
||||
// et vérifier qu'elle est ouverte avant accès
|
||||
Box<PassageModel> get _passageBox {
|
||||
_ensureBoxIsOpen();
|
||||
return Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
||||
if (_cachedPassageBox == null) {
|
||||
if (!Hive.isBoxOpen(AppKeys.passagesBoxName)) {
|
||||
throw Exception('La boîte ${AppKeys.passagesBoxName} n\'est pas ouverte. Initialisez d\'abord l\'application.');
|
||||
}
|
||||
_cachedPassageBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
||||
debugPrint('PassageRepository: Box ${AppKeys.passagesBoxName} mise en cache');
|
||||
}
|
||||
return _cachedPassageBox!;
|
||||
}
|
||||
|
||||
// Méthode pour réinitialiser le cache après modification de la box
|
||||
void _resetCache() {
|
||||
_cachedPassageBox = null;
|
||||
}
|
||||
|
||||
// Méthode pour exposer la Box Hive (nécessaire pour ValueListenableBuilder)
|
||||
@@ -55,14 +68,6 @@ class PassageRepository extends ChangeNotifier {
|
||||
bool get isLoading => _isLoading;
|
||||
List<PassageModel> get passages => getAllPassages();
|
||||
|
||||
// Méthode pour vérifier si la boîte est ouverte et l'ouvrir si nécessaire
|
||||
Future<void> _ensureBoxIsOpen() async {
|
||||
const boxName = AppKeys.passagesBoxName;
|
||||
if (!Hive.isBoxOpen(boxName)) {
|
||||
debugPrint('Ouverture de la boîte $boxName dans PassageRepository...');
|
||||
await Hive.openBox<PassageModel>(boxName);
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer tous les passages
|
||||
List<PassageModel> getAllPassages() {
|
||||
@@ -78,6 +83,11 @@ class PassageRepository extends ChangeNotifier {
|
||||
List<PassageModel> getPassagesBySectorId(int sectorId) {
|
||||
return _passageBox.values.where((passage) => passage.fkSector == sectorId).toList();
|
||||
}
|
||||
|
||||
// Récupérer les passages orphelins (sans secteur)
|
||||
List<PassageModel> getOrphanPassages() {
|
||||
return _passageBox.values.where((passage) => passage.fkSector == null).toList();
|
||||
}
|
||||
|
||||
// Récupérer les passages par type
|
||||
List<PassageModel> getPassagesByType(int type) {
|
||||
@@ -102,10 +112,13 @@ class PassageRepository extends ChangeNotifier {
|
||||
// Récupérer les passages par date
|
||||
List<PassageModel> getPassagesByDate(DateTime date) {
|
||||
return _passageBox.values.where((passage) {
|
||||
// Ignorer les passages sans date
|
||||
if (passage.passedAt == null) return false;
|
||||
|
||||
final passageDate = DateTime(
|
||||
passage.passedAt.year,
|
||||
passage.passedAt.month,
|
||||
passage.passedAt.day,
|
||||
passage.passedAt!.year,
|
||||
passage.passedAt!.month,
|
||||
passage.passedAt!.day,
|
||||
);
|
||||
final searchDate = DateTime(date.year, date.month, date.day);
|
||||
return passageDate.isAtSameMomentAs(searchDate);
|
||||
@@ -115,15 +128,24 @@ class PassageRepository extends ChangeNotifier {
|
||||
// Sauvegarder un passage
|
||||
Future<void> savePassage(PassageModel passage) async {
|
||||
await _passageBox.put(passage.id, passage);
|
||||
_resetCache(); // Réinitialiser le cache après modification
|
||||
notifyListeners();
|
||||
_notifyPassageStream();
|
||||
}
|
||||
|
||||
// Sauvegarder plusieurs passages
|
||||
Future<void> savePassages(List<PassageModel> passages) async {
|
||||
for (final passage in passages) {
|
||||
await _passageBox.put(passage.id, passage);
|
||||
}
|
||||
if (passages.isEmpty) return;
|
||||
|
||||
// Créer une map avec l'ID comme clé pour putAll
|
||||
final Map<dynamic, PassageModel> passagesMap = {
|
||||
for (final passage in passages) passage.id: passage
|
||||
};
|
||||
|
||||
// Sauvegarder tous les passages en une seule opération
|
||||
await _passageBox.putAll(passagesMap);
|
||||
|
||||
_resetCache(); // Réinitialiser le cache après modification massive
|
||||
notifyListeners();
|
||||
_notifyPassageStream();
|
||||
}
|
||||
@@ -131,6 +153,7 @@ class PassageRepository extends ChangeNotifier {
|
||||
// Supprimer un passage
|
||||
Future<void> deletePassage(int id) async {
|
||||
await _passageBox.delete(id);
|
||||
_resetCache(); // Réinitialiser le cache après suppression
|
||||
notifyListeners();
|
||||
_notifyPassageStream();
|
||||
}
|
||||
@@ -269,6 +292,7 @@ class PassageRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
debugPrint('$count passages traités et stockés');
|
||||
_resetCache(); // Réinitialiser le cache après traitement des données API
|
||||
notifyListeners();
|
||||
_notifyPassageStream();
|
||||
} catch (e) {
|
||||
@@ -361,6 +385,7 @@ class PassageRepository extends ChangeNotifier {
|
||||
// Vider tous les passages
|
||||
Future<void> clearAllPassages() async {
|
||||
await _passageBox.clear();
|
||||
_resetCache(); // Réinitialiser le cache après suppression de toutes les données
|
||||
notifyListeners();
|
||||
_notifyPassageStream();
|
||||
}
|
||||
|
||||
0
app/lib/core/repositories/region_repository.dart
Normal file → Executable file
0
app/lib/core/repositories/region_repository.dart
Normal file → Executable file
369
app/lib/core/repositories/sector_repository.dart
Normal file → Executable file
369
app/lib/core/repositories/sector_repository.dart
Normal file → Executable file
@@ -1,30 +1,37 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:geosector_app/core/data/models/sector_model.dart';
|
||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/core/repositories/passage_repository.dart';
|
||||
import 'package:geosector_app/core/services/data_loading_service.dart';
|
||||
|
||||
class SectorRepository extends ChangeNotifier {
|
||||
// Constructeur sans paramètres - utilise ApiService.instance
|
||||
SectorRepository();
|
||||
// Cache de la box pour éviter les vérifications répétées
|
||||
Box<SectorModel>? _cachedSectorBox;
|
||||
|
||||
// Utiliser un getter lazy pour n'accéder à la boîte que lorsque nécessaire
|
||||
// et vérifier qu'elle est ouverte avant accès
|
||||
Box<SectorModel> get _sectorBox {
|
||||
_ensureBoxIsOpen();
|
||||
return Hive.box<SectorModel>(AppKeys.sectorsBoxName);
|
||||
if (_cachedSectorBox == null) {
|
||||
if (!Hive.isBoxOpen(AppKeys.sectorsBoxName)) {
|
||||
throw Exception('La boîte ${AppKeys.sectorsBoxName} n\'est pas ouverte. Initialisez d\'abord l\'application.');
|
||||
}
|
||||
_cachedSectorBox = Hive.box<SectorModel>(AppKeys.sectorsBoxName);
|
||||
debugPrint('SectorRepository: Box ${AppKeys.sectorsBoxName} mise en cache');
|
||||
}
|
||||
return _cachedSectorBox!;
|
||||
}
|
||||
|
||||
// Constante pour l'ID par défaut
|
||||
static const int defaultSectorId = 1;
|
||||
|
||||
// Méthode pour vérifier si la boîte est ouverte et l'ouvrir si nécessaire
|
||||
Future<void> _ensureBoxIsOpen() async {
|
||||
debugPrint('SectorRepository: Vérification de l\'ouverture de la boîte ${AppKeys.sectorsBoxName}...');
|
||||
const boxName = AppKeys.sectorsBoxName;
|
||||
if (!Hive.isBoxOpen(boxName)) {
|
||||
debugPrint('Ouverture de la boîte $boxName dans SectorRepository...');
|
||||
await Hive.openBox<SectorModel>(boxName);
|
||||
}
|
||||
// Méthode pour réinitialiser le cache après modification de la box
|
||||
void _resetCache() {
|
||||
_cachedSectorBox = null;
|
||||
}
|
||||
|
||||
// Récupérer tous les secteurs
|
||||
@@ -40,12 +47,14 @@ class SectorRepository extends ChangeNotifier {
|
||||
// Sauvegarder un secteur
|
||||
Future<void> saveSector(SectorModel sector) async {
|
||||
await _sectorBox.put(sector.id, sector);
|
||||
_resetCache(); // Réinitialiser le cache après modification
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Supprimer un secteur
|
||||
Future<void> deleteSector(int id) async {
|
||||
await _sectorBox.delete(id);
|
||||
_resetCache(); // Réinitialiser le cache après modification
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -58,6 +67,7 @@ class SectorRepository extends ChangeNotifier {
|
||||
for (final sector in sectors) {
|
||||
await _sectorBox.put(sector.id, sector);
|
||||
}
|
||||
_resetCache(); // Réinitialiser le cache après modification massive
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -98,6 +108,7 @@ class SectorRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
debugPrint('$count secteurs traités et stockés');
|
||||
_resetCache(); // Réinitialiser le cache après traitement des données API
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du traitement des secteurs: $e');
|
||||
@@ -127,63 +138,355 @@ class SectorRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// Créer un nouveau secteur via l'API
|
||||
Future<SectorModel?> createSector(SectorModel sector) async {
|
||||
Future<Map<String, dynamic>> createSector(SectorModel sector, {required List<int> users, required int fkEntite, required int operationId}) async {
|
||||
try {
|
||||
// Préparer les données à envoyer
|
||||
final Map<String, dynamic> requestData = {
|
||||
...sector.toJson(),
|
||||
'users': users,
|
||||
'fk_entite': fkEntite,
|
||||
'operation_id': operationId,
|
||||
};
|
||||
|
||||
final response = await ApiService.instance.post(
|
||||
AppKeys.sectorsEndpoint,
|
||||
data: sector.toJson(),
|
||||
data: requestData,
|
||||
);
|
||||
final Map<String, dynamic> responseData = response as Map<String, dynamic>;
|
||||
|
||||
// Gérer la réponse correctement
|
||||
final dynamic responseRaw = response is Response ? response.data : response;
|
||||
final Map<String, dynamic> responseData = Map<String, dynamic>.from(responseRaw as Map);
|
||||
|
||||
if (responseData['status'] == 'success' && responseData['sector'] != null) {
|
||||
final SectorModel newSector = SectorModel.fromJson(responseData['sector']);
|
||||
if (responseData['status'] == 'success') {
|
||||
// L'API peut retourner soit 'sector' (objet complet) soit 'sector_id' (ID seulement)
|
||||
SectorModel newSector;
|
||||
|
||||
if (responseData['sector'] != null) {
|
||||
// Cas où l'API retourne l'objet secteur complet
|
||||
newSector = SectorModel.fromJson(responseData['sector']);
|
||||
} else if (responseData['sector_id'] != null) {
|
||||
// Cas où l'API retourne seulement l'ID du secteur créé
|
||||
final sectorId = responseData['sector_id'] is String
|
||||
? int.parse(responseData['sector_id'])
|
||||
: responseData['sector_id'] as int;
|
||||
|
||||
// Créer le secteur avec les données envoyées et l'ID reçu
|
||||
newSector = sector.copyWith(id: sectorId);
|
||||
} else {
|
||||
debugPrint('Erreur: Aucune donnée de secteur dans la réponse');
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Aucune donnée de secteur dans la réponse'
|
||||
};
|
||||
}
|
||||
|
||||
// Sauvegarder le secteur
|
||||
await saveSector(newSector);
|
||||
return newSector;
|
||||
|
||||
// Traiter les passages retournés s'ils existent
|
||||
if (responseData['passages_sector'] != null) {
|
||||
try {
|
||||
final passagesData = responseData['passages_sector'] as List<dynamic>;
|
||||
debugPrint('Traitement de ${passagesData.length} passages retournés');
|
||||
|
||||
// Utiliser PassageRepository pour traiter les passages
|
||||
final passageRepository = PassageRepository();
|
||||
|
||||
// Convertir chaque passage au format complet attendu
|
||||
final List<PassageModel> passagesToSave = [];
|
||||
for (final passageData in passagesData) {
|
||||
try {
|
||||
// Caster passageData en Map<String, dynamic>
|
||||
final Map<String, dynamic> passageDataMap = Map<String, dynamic>.from(passageData as Map);
|
||||
|
||||
// L'API retourne déjà des passages complets, on les utilise directement
|
||||
final passage = PassageModel.fromJson(passageDataMap);
|
||||
passagesToSave.add(passage);
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du traitement d\'un passage: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Sauvegarder tous les passages
|
||||
if (passagesToSave.isNotEmpty) {
|
||||
await passageRepository.savePassages(passagesToSave);
|
||||
debugPrint('${passagesToSave.length} passages sauvegardés');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du traitement des passages: $e');
|
||||
// Ne pas faire échouer la création du secteur si le traitement des passages échoue
|
||||
}
|
||||
}
|
||||
|
||||
// Traiter les users_sectors retournés s'ils existent
|
||||
if (responseData['users_sectors'] != null) {
|
||||
try {
|
||||
final usersSectorsData = responseData['users_sectors'] as List<dynamic>;
|
||||
debugPrint('Traitement de ${usersSectorsData.length} associations utilisateur-secteur');
|
||||
|
||||
// Sauvegarder les associations dans la box UserSector via DataLoadingService
|
||||
await DataLoadingService.instance.processUserSectorsFromApi(usersSectorsData);
|
||||
|
||||
for (final userData in usersSectorsData) {
|
||||
debugPrint('Utilisateur ${userData['first_name']} ${userData['name']} (ID: ${userData['id']}) assigné au secteur ${userData['fk_sector']}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du traitement des users_sectors: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Afficher les statistiques si disponibles
|
||||
if (responseData['passages_created'] != null || responseData['passages_integrated'] != null) {
|
||||
final created = responseData['passages_created'] ?? 0;
|
||||
final integrated = responseData['passages_integrated'] ?? 0;
|
||||
debugPrint('Statistiques: $created passages créés, $integrated passages intégrés');
|
||||
}
|
||||
|
||||
// Retourner le secteur et toutes les informations
|
||||
return {
|
||||
'status': 'success',
|
||||
'sector': newSector,
|
||||
'passages_created': responseData['passages_created'] ?? 0,
|
||||
'passages_integrated': responseData['passages_integrated'] ?? 0,
|
||||
'passages_total': (responseData['passages_created'] ?? 0) + (responseData['passages_integrated'] ?? 0),
|
||||
'warning': responseData['warning'],
|
||||
'intersecting_departments': responseData['intersecting_departments'],
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': responseData['message'] ?? 'Erreur lors de la création'
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
debugPrint('Erreur lors de la création du secteur: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour un secteur via l'API
|
||||
Future<SectorModel?> updateSector(SectorModel sector) async {
|
||||
Future<Map<String, dynamic>> updateSector(SectorModel sector, {List<int>? users}) async {
|
||||
try {
|
||||
// Préparer les données à envoyer
|
||||
final Map<String, dynamic> requestData = {
|
||||
...sector.toJson(),
|
||||
};
|
||||
|
||||
// Ajouter les utilisateurs si fournis
|
||||
if (users != null) {
|
||||
requestData['users'] = users;
|
||||
}
|
||||
|
||||
final response = await ApiService.instance.put(
|
||||
'${AppKeys.sectorsEndpoint}/${sector.id}',
|
||||
data: sector.toJson(),
|
||||
data: requestData,
|
||||
);
|
||||
final Map<String, dynamic> responseData = response as Map<String, dynamic>;
|
||||
|
||||
// Gérer la réponse correctement
|
||||
final dynamic responseRaw = response is Response ? response.data : response;
|
||||
final Map<String, dynamic> responseData = Map<String, dynamic>.from(responseRaw as Map);
|
||||
|
||||
if (responseData['status'] == 'success' && responseData['sector'] != null) {
|
||||
final SectorModel updatedSector = SectorModel.fromJson(responseData['sector']);
|
||||
await saveSector(updatedSector);
|
||||
return updatedSector;
|
||||
if (responseData['status'] == 'success') {
|
||||
// Sauvegarder le secteur mis à jour
|
||||
if (responseData['sector'] != null) {
|
||||
final SectorModel updatedSector = SectorModel.fromJson(responseData['sector']);
|
||||
await saveSector(updatedSector);
|
||||
}
|
||||
|
||||
// Traiter les passages retournés s'ils existent
|
||||
if (responseData['passages_sector'] != null) {
|
||||
try {
|
||||
final passagesData = responseData['passages_sector'] as List<dynamic>;
|
||||
debugPrint('Traitement de ${passagesData.length} passages après UPDATE');
|
||||
|
||||
// Utiliser PassageRepository pour traiter les passages
|
||||
final passageRepository = PassageRepository();
|
||||
|
||||
// Vider d'abord tous les passages du secteur
|
||||
await _deleteAllPassagesOfSector(sector.id);
|
||||
|
||||
// Puis sauvegarder tous les passages retournés
|
||||
final List<PassageModel> passagesToSave = [];
|
||||
for (final passageData in passagesData) {
|
||||
try {
|
||||
final Map<String, dynamic> passageDataMap = Map<String, dynamic>.from(passageData as Map);
|
||||
final passage = PassageModel.fromJson(passageDataMap);
|
||||
passagesToSave.add(passage);
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du traitement d\'un passage: $e');
|
||||
}
|
||||
}
|
||||
|
||||
if (passagesToSave.isNotEmpty) {
|
||||
await passageRepository.savePassages(passagesToSave);
|
||||
debugPrint('${passagesToSave.length} passages sauvegardés après UPDATE');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du traitement des passages: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Traiter les users_sectors retournés s'ils existent
|
||||
if (responseData['users_sectors'] != null) {
|
||||
try {
|
||||
final usersSectorsData = responseData['users_sectors'] as List<dynamic>;
|
||||
debugPrint('Traitement de ${usersSectorsData.length} associations utilisateur-secteur');
|
||||
|
||||
// Sauvegarder les associations dans la box UserSector via DataLoadingService
|
||||
await DataLoadingService.instance.processUserSectorsFromApi(usersSectorsData);
|
||||
|
||||
for (final userData in usersSectorsData) {
|
||||
debugPrint('Utilisateur ${userData['first_name']} ${userData['name']} (ID: ${userData['id']}) assigné au secteur ${userData['fk_sector']}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du traitement des users_sectors: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Afficher les statistiques
|
||||
final orphaned = responseData['passages_orphaned'] ?? 0;
|
||||
final updated = responseData['passages_updated'] ?? 0;
|
||||
final created = responseData['passages_created'] ?? 0;
|
||||
final total = responseData['passages_total'] ?? 0;
|
||||
debugPrint('Statistiques UPDATE: $orphaned orphelins, $updated mis à jour, $created créés, $total total');
|
||||
|
||||
// Retourner toutes les informations
|
||||
return {
|
||||
'status': 'success',
|
||||
'sector': responseData['sector'] != null ? SectorModel.fromJson(responseData['sector']) : null,
|
||||
'passages_orphaned': orphaned,
|
||||
'passages_updated': updated,
|
||||
'passages_created': created,
|
||||
'passages_total': total,
|
||||
'warning': responseData['warning'],
|
||||
'intersecting_departments': responseData['intersecting_departments'],
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': responseData['message'] ?? 'Erreur lors de la mise à jour'
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
debugPrint('Erreur lors de la mise à jour du secteur: $e');
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Erreur de connexion au serveur'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer un secteur via l'API
|
||||
Future<bool> deleteSectorFromApi(int id) async {
|
||||
Future<Map<String, dynamic>> deleteSectorFromApi(int id) async {
|
||||
try {
|
||||
final response = await ApiService.instance.delete(
|
||||
'${AppKeys.sectorsEndpoint}/$id',
|
||||
);
|
||||
final Map<String, dynamic> responseData = response as Map<String, dynamic>;
|
||||
final Map<String, dynamic> responseData = response.data as Map<String, dynamic>;
|
||||
|
||||
if (responseData['status'] == 'success') {
|
||||
// 1. Supprimer tous les passages de ce secteur dans Hive
|
||||
await _deleteAllPassagesOfSector(id);
|
||||
|
||||
// 2. Supprimer le secteur de Hive
|
||||
await deleteSector(id);
|
||||
return true;
|
||||
|
||||
// 3. Importer les passages orphelins retournés par l'API
|
||||
if (responseData['passages_sector'] != null) {
|
||||
await _importOrphanPassages(responseData['passages_sector'] as List<dynamic>);
|
||||
}
|
||||
|
||||
// Vérifier que le secteur a bien été supprimé
|
||||
final deletedSector = getSectorById(id);
|
||||
if (deletedSector != null) {
|
||||
debugPrint('ATTENTION: Le secteur $id existe encore après suppression!');
|
||||
} else {
|
||||
debugPrint('Secteur $id supprimé avec succès de Hive');
|
||||
}
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'passages_deleted': responseData['passages_deleted'] ?? 0,
|
||||
'passages_reassigned': responseData['passages_reassigned'] ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': responseData['message'] ?? 'Erreur lors de la suppression',
|
||||
};
|
||||
} catch (e) {
|
||||
return false;
|
||||
debugPrint('Erreur lors de la suppression du secteur: $e');
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Erreur de connexion au serveur',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer tous les passages d'un secteur
|
||||
Future<void> _deleteAllPassagesOfSector(int sectorId) async {
|
||||
try {
|
||||
if (!Hive.isBoxOpen(AppKeys.passagesBoxName)) {
|
||||
debugPrint('La boîte des passages n\'est pas ouverte');
|
||||
return;
|
||||
}
|
||||
|
||||
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
||||
final List<dynamic> keysToDelete = [];
|
||||
|
||||
// Identifier toutes les clés des passages du secteur
|
||||
for (final entry in passagesBox.toMap().entries) {
|
||||
final passage = entry.value;
|
||||
if (passage.fkSector == sectorId) {
|
||||
keysToDelete.add(entry.key);
|
||||
}
|
||||
}
|
||||
|
||||
if (keysToDelete.isEmpty) {
|
||||
debugPrint('Aucun passage à supprimer pour le secteur $sectorId');
|
||||
return;
|
||||
}
|
||||
|
||||
// Supprimer tous les passages en une seule opération
|
||||
await passagesBox.deleteAll(keysToDelete);
|
||||
|
||||
debugPrint('${keysToDelete.length} passages supprimés du secteur $sectorId en une seule opération');
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la suppression des passages: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Importer les passages orphelins après suppression d'un secteur
|
||||
Future<void> _importOrphanPassages(List<dynamic> passagesData) async {
|
||||
try {
|
||||
if (passagesData.isEmpty) {
|
||||
debugPrint('Aucun passage orphelin à importer');
|
||||
return;
|
||||
}
|
||||
|
||||
final passageRepository = PassageRepository();
|
||||
final List<PassageModel> passagesToSave = [];
|
||||
|
||||
for (final passageData in passagesData) {
|
||||
try {
|
||||
// Les passages orphelins ont fk_sector = null
|
||||
final Map<String, dynamic> passageDataMap = Map<String, dynamic>.from(passageData as Map);
|
||||
final passage = PassageModel.fromJson(passageDataMap);
|
||||
passagesToSave.add(passage);
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du traitement d\'un passage orphelin: $e');
|
||||
}
|
||||
}
|
||||
|
||||
if (passagesToSave.isNotEmpty) {
|
||||
await passageRepository.savePassages(passagesToSave);
|
||||
debugPrint('${passagesToSave.length} passages orphelins importés avec fk_sector = null');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de l\'importation des passages orphelins: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
81
app/lib/core/repositories/user_repository.dart
Normal file → Executable file
81
app/lib/core/repositories/user_repository.dart
Normal file → Executable file
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
@@ -59,7 +58,9 @@ class UserRepository extends ChangeNotifier {
|
||||
List<OperationModel> get operations {
|
||||
try {
|
||||
if (Hive.isBoxOpen(AppKeys.operationsBoxName)) {
|
||||
return Hive.box<OperationModel>(AppKeys.operationsBoxName).values.toList();
|
||||
return Hive.box<OperationModel>(AppKeys.operationsBoxName)
|
||||
.values
|
||||
.toList();
|
||||
}
|
||||
return [];
|
||||
} catch (e) {
|
||||
@@ -142,7 +143,8 @@ class UserRepository extends ChangeNotifier {
|
||||
// === AUTHENTIFICATION ===
|
||||
|
||||
/// Login API PHP
|
||||
Future<Map<String, dynamic>> loginAPI(String username, String password, {required String type}) async {
|
||||
Future<Map<String, dynamic>> loginAPI(String username, String password,
|
||||
{required String type}) async {
|
||||
try {
|
||||
return await ApiService.instance.login(username, password, type: type);
|
||||
} catch (e) {
|
||||
@@ -152,11 +154,19 @@ class UserRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Register API PHP - Uniquement pour les administrateurs
|
||||
Future<Map<String, dynamic>> registerAPI(String email, String name, String amicaleName, String postalCode, String cityName) async {
|
||||
Future<Map<String, dynamic>> registerAPI(String email, String name,
|
||||
String amicaleName, String postalCode, String cityName) async {
|
||||
try {
|
||||
final Map<String, dynamic> data = {'email': email, 'name': name, 'amicale_name': amicaleName, 'postal_code': postalCode, 'city_name': cityName};
|
||||
final Map<String, dynamic> data = {
|
||||
'email': email,
|
||||
'name': name,
|
||||
'amicale_name': amicaleName,
|
||||
'postal_code': postalCode,
|
||||
'city_name': cityName
|
||||
};
|
||||
|
||||
final response = await ApiService.instance.post(AppKeys.registerEndpoint, data: data);
|
||||
final response =
|
||||
await ApiService.instance.post(AppKeys.registerEndpoint, data: data);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur register API: $e');
|
||||
@@ -175,7 +185,8 @@ class UserRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Méthode d'inscription (uniquement pour les administrateurs)
|
||||
Future<bool> register(String email, String password, String name, String amicaleName, String postalCode, String cityName) async {
|
||||
Future<bool> register(String email, String password, String name,
|
||||
String amicaleName, String postalCode, String cityName) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
@@ -183,10 +194,13 @@ class UserRepository extends ChangeNotifier {
|
||||
debugPrint('📝 Tentative d\'inscription: $email');
|
||||
|
||||
// Enregistrer l'administrateur via l'API
|
||||
final apiResult = await registerAPI(email, name, amicaleName, postalCode, cityName);
|
||||
final apiResult =
|
||||
await registerAPI(email, name, amicaleName, postalCode, cityName);
|
||||
|
||||
// Créer l'administrateur local
|
||||
final int userId = apiResult['user_id'] is String ? int.parse(apiResult['user_id']) : apiResult['user_id'];
|
||||
final int userId = apiResult['user_id'] is String
|
||||
? int.parse(apiResult['user_id'])
|
||||
: apiResult['user_id'];
|
||||
final now = DateTime.now();
|
||||
final newAdmin = UserModel(
|
||||
id: userId,
|
||||
@@ -220,7 +234,8 @@ class UserRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Connexion simplifiée avec DataLoadingService
|
||||
Future<bool> login(String username, String password, {required String type}) async {
|
||||
Future<bool> login(String username, String password,
|
||||
{required String type}) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
@@ -242,8 +257,10 @@ class UserRepository extends ChangeNotifier {
|
||||
// Étape 3: Traitement des données utilisateur (35%)
|
||||
debugPrint('👤 Traitement des données utilisateur...');
|
||||
|
||||
if (apiResult['user'] != null && apiResult['user'] is Map<String, dynamic>) {
|
||||
final user = _processUserData(apiResult['user'] as Map<String, dynamic>, apiResult['session_id'], apiResult['session_expiry']);
|
||||
if (apiResult['user'] != null &&
|
||||
apiResult['user'] is Map<String, dynamic>) {
|
||||
final user = _processUserData(apiResult['user'] as Map<String, dynamic>,
|
||||
apiResult['session_id'], apiResult['session_expiry']);
|
||||
|
||||
// Sauvegarder via le service
|
||||
await CurrentUserService.instance.setUser(user);
|
||||
@@ -308,17 +325,10 @@ class UserRepository extends ChangeNotifier {
|
||||
// Réinitialiser l'état de HiveResetStateService
|
||||
hiveResetStateService.reset();
|
||||
|
||||
if (context.mounted) {
|
||||
context.go('/');
|
||||
}
|
||||
|
||||
debugPrint('✅ Déconnexion réussie');
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur déconnexion: $e');
|
||||
if (context.mounted) {
|
||||
context.go('/'); // Forcer la redirection même en cas d'erreur
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
@@ -327,7 +337,9 @@ class UserRepository extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Connexion avec interface utilisateur et progression
|
||||
Future<bool> loginWithUI(BuildContext context, String username, String password, {required String type}) async {
|
||||
Future<bool> loginWithUI(
|
||||
BuildContext context, String username, String password,
|
||||
{required String type}) async {
|
||||
try {
|
||||
// Créer et afficher l'overlay de progression
|
||||
_progressOverlay = LoadingProgressOverlayUtils.show(
|
||||
@@ -389,7 +401,8 @@ class UserRepository extends ChangeNotifier {
|
||||
// === ACCESSEURS DÉLÉGUÉS AUX SERVICES ===
|
||||
|
||||
/// Simplifier les getters d'amicale
|
||||
AmicaleModel? getCurrentUserAmicale() => CurrentAmicaleService.instance.currentAmicale;
|
||||
AmicaleModel? getCurrentUserAmicale() =>
|
||||
CurrentAmicaleService.instance.currentAmicale;
|
||||
|
||||
/// Obtenir tous les utilisateurs locaux
|
||||
List<UserModel> getAllUsers() {
|
||||
@@ -543,7 +556,8 @@ class UserRepository extends ChangeNotifier {
|
||||
/// Créer ou mettre à jour une amicale localement
|
||||
Future<AmicaleModel> saveAmicale(AmicaleModel amicale) async {
|
||||
if (Hive.isBoxOpen(AppKeys.amicaleBoxName)) {
|
||||
await Hive.box<AmicaleModel>(AppKeys.amicaleBoxName).put(amicale.id, amicale);
|
||||
await Hive.box<AmicaleModel>(AppKeys.amicaleBoxName)
|
||||
.put(amicale.id, amicale);
|
||||
notifyListeners();
|
||||
}
|
||||
return amicale;
|
||||
@@ -589,7 +603,8 @@ class UserRepository extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
final unsyncedUsers = _userBox.values.where((user) => !user.isSynced).toList();
|
||||
final unsyncedUsers =
|
||||
_userBox.values.where((user) => !user.isSynced).toList();
|
||||
|
||||
if (unsyncedUsers.isEmpty) {
|
||||
return;
|
||||
@@ -640,7 +655,8 @@ class UserRepository extends ChangeNotifier {
|
||||
// === TRAITEMENT DES DONNÉES UTILISATEUR ===
|
||||
|
||||
/// Méthode pour traiter les données utilisateur reçues de l'API
|
||||
UserModel _processUserData(Map<String, dynamic> userData, String? sessionId, String? sessionExpiry) {
|
||||
UserModel _processUserData(
|
||||
Map<String, dynamic> userData, String? sessionId, String? sessionExpiry) {
|
||||
debugPrint('👤 Traitement des données utilisateur: ${userData.toString()}');
|
||||
|
||||
// Convertir l'ID en int
|
||||
@@ -660,15 +676,20 @@ class UserRepository extends ChangeNotifier {
|
||||
|
||||
// Convertir fk_entite en int si présent
|
||||
final dynamic rawFkEntite = userData['fk_entite'];
|
||||
final int? fkEntite = rawFkEntite != null ? (rawFkEntite is String ? int.parse(rawFkEntite) : rawFkEntite as int) : null;
|
||||
final int? fkEntite = rawFkEntite != null
|
||||
? (rawFkEntite is String ? int.parse(rawFkEntite) : rawFkEntite as int)
|
||||
: null;
|
||||
|
||||
// Convertir fk_titre en int si présent
|
||||
final dynamic rawFkTitre = userData['fk_titre'];
|
||||
final int? fkTitre = rawFkTitre != null ? (rawFkTitre is String ? int.parse(rawFkTitre) : rawFkTitre as int) : null;
|
||||
final int? fkTitre = rawFkTitre != null
|
||||
? (rawFkTitre is String ? int.parse(rawFkTitre) : rawFkTitre as int)
|
||||
: null;
|
||||
|
||||
// Traiter les dates si présentes
|
||||
DateTime? dateNaissance;
|
||||
if (userData['date_naissance'] != null && userData['date_naissance'] != '') {
|
||||
if (userData['date_naissance'] != null &&
|
||||
userData['date_naissance'] != '') {
|
||||
try {
|
||||
dateNaissance = DateTime.parse(userData['date_naissance']);
|
||||
} catch (e) {
|
||||
@@ -685,7 +706,8 @@ class UserRepository extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ Données traitées - id: $id, role: $role, fkEntite: $fkEntite');
|
||||
debugPrint(
|
||||
'✅ Données traitées - id: $id, role: $role, fkEntite: $fkEntite');
|
||||
|
||||
// Créer un utilisateur avec toutes les données disponibles
|
||||
return UserModel(
|
||||
@@ -700,7 +722,8 @@ class UserRepository extends ChangeNotifier {
|
||||
isActive: true,
|
||||
isSynced: true,
|
||||
sessionId: sessionId,
|
||||
sessionExpiry: sessionExpiry != null ? DateTime.parse(sessionExpiry) : null,
|
||||
sessionExpiry:
|
||||
sessionExpiry != null ? DateTime.parse(sessionExpiry) : null,
|
||||
sectName: userData['sect_name'],
|
||||
fkEntite: fkEntite,
|
||||
fkTitre: fkTitre,
|
||||
|
||||
0
app/lib/core/services/api_service.dart
Normal file → Executable file
0
app/lib/core/services/api_service.dart
Normal file → Executable file
0
app/lib/core/services/app_info_service.dart
Normal file → Executable file
0
app/lib/core/services/app_info_service.dart
Normal file → Executable file
0
app/lib/core/services/connectivity_service.dart
Normal file → Executable file
0
app/lib/core/services/connectivity_service.dart
Normal file → Executable file
24
app/lib/core/services/current_amicale_service.dart
Normal file → Executable file
24
app/lib/core/services/current_amicale_service.dart
Normal file → Executable file
@@ -6,7 +6,8 @@ import 'package:geosector_app/core/services/current_user_service.dart';
|
||||
|
||||
class CurrentAmicaleService extends ChangeNotifier {
|
||||
static CurrentAmicaleService? _instance;
|
||||
static CurrentAmicaleService get instance => _instance ??= CurrentAmicaleService._internal();
|
||||
static CurrentAmicaleService get instance =>
|
||||
_instance ??= CurrentAmicaleService._internal();
|
||||
CurrentAmicaleService._internal();
|
||||
|
||||
AmicaleModel? _currentAmicale;
|
||||
@@ -23,7 +24,8 @@ class CurrentAmicaleService extends ChangeNotifier {
|
||||
? '${_currentAmicale!.adresse1} ${_currentAmicale!.adresse2}'.trim()
|
||||
: null;
|
||||
String? get amicaleFullAddress => _currentAmicale != null
|
||||
? '${amicaleAddress ?? ''} ${_currentAmicale!.codePostal} ${_currentAmicale!.ville}'.trim()
|
||||
? '${amicaleAddress ?? ''} ${_currentAmicale!.codePostal} ${_currentAmicale!.ville}'
|
||||
.trim()
|
||||
: null;
|
||||
bool get amicaleIsActive => _currentAmicale?.chkActive ?? false;
|
||||
bool get isClient => _currentAmicale?.fkType == 1;
|
||||
@@ -33,13 +35,11 @@ class CurrentAmicaleService extends ChangeNotifier {
|
||||
_currentAmicale?.gpsLat.isNotEmpty == true &&
|
||||
_currentAmicale?.gpsLng.isNotEmpty == true;
|
||||
|
||||
double? get latitude => hasGpsCoordinates
|
||||
? double.tryParse(_currentAmicale!.gpsLat)
|
||||
: null;
|
||||
double? get latitude =>
|
||||
hasGpsCoordinates ? double.tryParse(_currentAmicale!.gpsLat) : null;
|
||||
|
||||
double? get longitude => hasGpsCoordinates
|
||||
? double.tryParse(_currentAmicale!.gpsLng)
|
||||
: null;
|
||||
double? get longitude =>
|
||||
hasGpsCoordinates ? double.tryParse(_currentAmicale!.gpsLng) : null;
|
||||
|
||||
// === SETTERS ===
|
||||
Future<void> setAmicale(AmicaleModel? amicale) async {
|
||||
@@ -85,7 +85,7 @@ class CurrentAmicaleService extends ChangeNotifier {
|
||||
} else {
|
||||
// Si l'amicale n'est pas la bonne, la chercher ou l'effacer
|
||||
_currentAmicale = null;
|
||||
debugPrint('⚠️ Amicale ${amicaleId} non trouvée dans Hive');
|
||||
debugPrint('⚠️ Amicale $amicaleId non trouvée dans Hive');
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
@@ -119,7 +119,7 @@ class CurrentAmicaleService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> loadFromHive() async {
|
||||
try {
|
||||
try {
|
||||
final box = Hive.box<AmicaleModel>(AppKeys.amicaleBoxName);
|
||||
_currentAmicale = box.get('current_amicale');
|
||||
|
||||
@@ -133,7 +133,7 @@ class CurrentAmicaleService extends ChangeNotifier {
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur chargement amicale depuis Hive: $e');
|
||||
_currentAmicale = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === RESET POUR TESTS ===
|
||||
@@ -141,4 +141,4 @@ class CurrentAmicaleService extends ChangeNotifier {
|
||||
_instance?._currentAmicale = null;
|
||||
_instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0
app/lib/core/services/current_user_service.dart
Normal file → Executable file
0
app/lib/core/services/current_user_service.dart
Normal file → Executable file
159
app/lib/core/services/data_loading_service.dart
Normal file → Executable file
159
app/lib/core/services/data_loading_service.dart
Normal file → Executable file
@@ -3,7 +3,6 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart';
|
||||
import 'package:geosector_app/core/services/hive_web_fix.dart';
|
||||
import 'package:geosector_app/core/data/models/operation_model.dart';
|
||||
import 'package:geosector_app/core/data/models/sector_model.dart';
|
||||
@@ -12,7 +11,6 @@ import 'package:geosector_app/core/data/models/membre_model.dart';
|
||||
import 'package:geosector_app/core/data/models/user_sector_model.dart';
|
||||
import 'package:geosector_app/core/data/models/amicale_model.dart';
|
||||
import 'package:geosector_app/core/repositories/client_repository.dart';
|
||||
import 'package:geosector_app/core/repositories/amicale_repository.dart';
|
||||
import 'package:geosector_app/chat/models/conversation_model.dart';
|
||||
import 'package:geosector_app/chat/models/message_model.dart';
|
||||
import 'package:geosector_app/core/models/loading_state.dart';
|
||||
@@ -20,7 +18,8 @@ import 'package:geosector_app/core/models/loading_state.dart';
|
||||
/// Service singleton pour gérer le chargement et la gestion des données au login
|
||||
class DataLoadingService extends ChangeNotifier {
|
||||
static DataLoadingService? _instance;
|
||||
static DataLoadingService get instance => _instance ??= DataLoadingService._internal();
|
||||
static DataLoadingService get instance =>
|
||||
_instance ??= DataLoadingService._internal();
|
||||
DataLoadingService._internal();
|
||||
|
||||
// État du chargement
|
||||
@@ -43,14 +42,22 @@ class DataLoadingService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// === GETTERS POUR LES BOXES ===
|
||||
Box<OperationModel> get _operationBox => Hive.box<OperationModel>(AppKeys.operationsBoxName);
|
||||
Box<SectorModel> get _sectorBox => Hive.box<SectorModel>(AppKeys.sectorsBoxName);
|
||||
Box<PassageModel> get _passageBox => Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
||||
Box<MembreModel> get _membreBox => Hive.box<MembreModel>(AppKeys.membresBoxName);
|
||||
Box<UserSectorModel> get _userSectorBox => Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
|
||||
Box<AmicaleModel> get _amicaleBox => Hive.box<AmicaleModel>(AppKeys.amicaleBoxName);
|
||||
Box<ConversationModel> get _chatConversationBox => Hive.box<ConversationModel>(AppKeys.chatConversationsBoxName);
|
||||
Box<MessageModel> get _chatMessageBox => Hive.box<MessageModel>(AppKeys.chatMessagesBoxName);
|
||||
Box<OperationModel> get _operationBox =>
|
||||
Hive.box<OperationModel>(AppKeys.operationsBoxName);
|
||||
Box<SectorModel> get _sectorBox =>
|
||||
Hive.box<SectorModel>(AppKeys.sectorsBoxName);
|
||||
Box<PassageModel> get _passageBox =>
|
||||
Hive.box<PassageModel>(AppKeys.passagesBoxName);
|
||||
Box<MembreModel> get _membreBox =>
|
||||
Hive.box<MembreModel>(AppKeys.membresBoxName);
|
||||
Box<UserSectorModel> get _userSectorBox =>
|
||||
Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
|
||||
Box<AmicaleModel> get _amicaleBox =>
|
||||
Hive.box<AmicaleModel>(AppKeys.amicaleBoxName);
|
||||
Box<ConversationModel> get _chatConversationBox =>
|
||||
Hive.box<ConversationModel>(AppKeys.chatConversationsBoxName);
|
||||
Box<MessageModel> get _chatMessageBox =>
|
||||
Hive.box<MessageModel>(AppKeys.chatMessagesBoxName);
|
||||
Box get _settingsBox => Hive.box(AppKeys.settingsBoxName);
|
||||
|
||||
/// Traite toutes les données reçues de l'API lors du login
|
||||
@@ -71,8 +78,8 @@ class DataLoadingService extends ChangeNotifier {
|
||||
await _processOperations(apiResult['operations']);
|
||||
}
|
||||
|
||||
if (apiResult['secteurs'] != null) {
|
||||
await _processSectors(apiResult['secteurs']);
|
||||
if (apiResult['sectors'] != null) {
|
||||
await _processSectors(apiResult['sectors']);
|
||||
}
|
||||
|
||||
if (apiResult['passages'] != null) {
|
||||
@@ -86,8 +93,16 @@ class DataLoadingService extends ChangeNotifier {
|
||||
await _processMembres(apiResult['membres']);
|
||||
}
|
||||
|
||||
if (apiResult['userSecteurs'] != null) {
|
||||
await _processUserSectors(apiResult['userSecteurs']);
|
||||
// ATTENTION : L'API envoie 'users_sectors', pas 'userSecteurs'
|
||||
if (apiResult['users_sectors'] != null) {
|
||||
debugPrint('📋 Traitement des associations users_sectors depuis le login');
|
||||
await _processUserSectors(apiResult['users_sectors'], clearAll: true);
|
||||
} else if (apiResult['userSecteurs'] != null) {
|
||||
// Fallback pour compatibilité si l'API change
|
||||
debugPrint('📋 Traitement des associations userSecteurs depuis le login (fallback)');
|
||||
await _processUserSectors(apiResult['userSecteurs'], clearAll: true);
|
||||
} else {
|
||||
debugPrint('⚠️ Aucune donnée users_sectors/userSecteurs trouvée dans la réponse du login');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur lors du chargement: $e');
|
||||
@@ -107,7 +122,8 @@ class DataLoadingService extends ChangeNotifier {
|
||||
|
||||
for (final boxName in requiredBoxes) {
|
||||
if (!Hive.isBoxOpen(boxName)) {
|
||||
throw Exception('La boîte $boxName n\'est pas ouverte. Redémarrez l\'application.');
|
||||
throw Exception(
|
||||
'La boîte $boxName n\'est pas ouverte. Redémarrez l\'application.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,8 +155,10 @@ class DataLoadingService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Méthode publique pour traiter les associations user-sectors depuis l'extérieur
|
||||
Future<void> processUserSectorsFromApi(dynamic userSectorsData) async {
|
||||
await _processUserSectors(userSectorsData);
|
||||
/// [clearAll] : si true, vide toute la box avant d'ajouter (pour le login)
|
||||
/// si false, ne supprime que les secteurs concernés (pour les mises à jour)
|
||||
Future<void> processUserSectorsFromApi(dynamic userSectorsData, {bool clearAll = false}) async {
|
||||
await _processUserSectors(userSectorsData, clearAll: clearAll);
|
||||
}
|
||||
|
||||
/// Méthode publique pour traiter les opérations depuis l'extérieur
|
||||
@@ -225,27 +243,41 @@ class DataLoadingService extends ChangeNotifier {
|
||||
List<dynamic> sectorsList;
|
||||
if (sectorsData is List) {
|
||||
sectorsList = sectorsData;
|
||||
debugPrint('📋 ${sectorsList.length} secteurs à traiter (format: List directe)');
|
||||
} else if (sectorsData is Map && sectorsData.containsKey('data')) {
|
||||
sectorsList = sectorsData['data'] as List<dynamic>;
|
||||
debugPrint('📋 ${sectorsList.length} secteurs à traiter (format: Map avec data)');
|
||||
} else {
|
||||
debugPrint('⚠️ Format de données de secteurs non reconnu');
|
||||
debugPrint('⚠️ Format de données de secteurs non reconnu: ${sectorsData.runtimeType}');
|
||||
debugPrint('⚠️ Contenu: ${sectorsData.toString().substring(0, 200)}...');
|
||||
return;
|
||||
}
|
||||
|
||||
await _sectorBox.clear();
|
||||
|
||||
int count = 0;
|
||||
int errorCount = 0;
|
||||
for (final sectorData in sectorsList) {
|
||||
try {
|
||||
debugPrint('🔄 Traitement secteur ID: ${sectorData['id']}, libelle: "${sectorData['libelle']}"');
|
||||
final sector = SectorModel.fromJson(sectorData);
|
||||
await _sectorBox.put(sector.id, sector);
|
||||
count++;
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur traitement secteur: $e');
|
||||
errorCount++;
|
||||
debugPrint('⚠️ Erreur traitement secteur ${sectorData['id']}: $e');
|
||||
debugPrint('⚠️ Données problématiques: $sectorData');
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ $count secteurs stockés');
|
||||
debugPrint('✅ $count secteurs stockés${errorCount > 0 ? ' ($errorCount erreurs ignorées)' : ''}');
|
||||
|
||||
// Vérification finale
|
||||
final storedSectors = _sectorBox.values.toList();
|
||||
debugPrint('🔍 Vérification: ${storedSectors.length} secteurs dans la box');
|
||||
for (final sector in storedSectors) {
|
||||
debugPrint(' - Secteur ${sector.id}: "${sector.libelle}" (${sector.color})');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur traitement secteurs: $e');
|
||||
}
|
||||
@@ -286,7 +318,8 @@ class DataLoadingService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ $count passages stockés${errorCount > 0 ? ' ($errorCount erreurs ignorées)' : ''}');
|
||||
debugPrint(
|
||||
'✅ $count passages stockés${errorCount > 0 ? ' ($errorCount erreurs ignorées)' : ''}');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur traitement passages: $e');
|
||||
}
|
||||
@@ -305,7 +338,8 @@ class DataLoadingService extends ChangeNotifier {
|
||||
await _amicaleBox.clear();
|
||||
try {
|
||||
// Les données d'amicale sont un objet unique
|
||||
final Map<String, dynamic> amicaleMap = Map<String, dynamic>.from(amicaleData as Map);
|
||||
final Map<String, dynamic> amicaleMap =
|
||||
Map<String, dynamic>.from(amicaleData as Map);
|
||||
final amicale = AmicaleModel.fromJson(amicaleMap);
|
||||
await _amicaleBox.put(amicale.id, amicale);
|
||||
debugPrint('✅ Amicale stockée: ${amicale.name} (ID: ${amicale.id})');
|
||||
@@ -353,15 +387,17 @@ class DataLoadingService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ $count membres stockés${errorCount > 0 ? ' ($errorCount erreurs ignorées)' : ''}');
|
||||
debugPrint(
|
||||
'✅ $count membres stockés${errorCount > 0 ? ' ($errorCount erreurs ignorées)' : ''}');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur traitement membres: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _processUserSectors(dynamic userSectorsData) async {
|
||||
Future<void> _processUserSectors(dynamic userSectorsData, {bool clearAll = false}) async {
|
||||
try {
|
||||
debugPrint('🔗 Traitement des associations utilisateurs-secteurs...');
|
||||
debugPrint('Type de données reçues: ${userSectorsData.runtimeType}');
|
||||
|
||||
if (userSectorsData == null) {
|
||||
debugPrint('ℹ️ Aucune association utilisateur-secteur à traiter');
|
||||
@@ -371,27 +407,91 @@ class DataLoadingService extends ChangeNotifier {
|
||||
List<dynamic> userSectorsList;
|
||||
if (userSectorsData is List) {
|
||||
userSectorsList = userSectorsData;
|
||||
} else if (userSectorsData is Map && userSectorsData.containsKey('data')) {
|
||||
debugPrint('✅ Données au format List avec ${userSectorsList.length} éléments');
|
||||
} else if (userSectorsData is Map &&
|
||||
userSectorsData.containsKey('data')) {
|
||||
userSectorsList = userSectorsData['data'] as List<dynamic>;
|
||||
debugPrint('✅ Données au format Map[data] avec ${userSectorsList.length} éléments');
|
||||
} else {
|
||||
debugPrint('⚠️ Format de données d\'associations non reconnu');
|
||||
return;
|
||||
}
|
||||
|
||||
await _userSectorBox.clear();
|
||||
// Vérifier si la box est ouverte
|
||||
if (!Hive.isBoxOpen(AppKeys.userSectorBoxName)) {
|
||||
debugPrint('❌ La box UserSector n\'est pas ouverte!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (clearAll) {
|
||||
// Mode login : on vide toute la box
|
||||
await _userSectorBox.clear();
|
||||
debugPrint('📦 Box UserSector vidée complètement (mode login)');
|
||||
} else {
|
||||
// Mode mise à jour : on ne supprime que les associations des secteurs concernés
|
||||
// car l'API ne retourne que les associations pour le(s) secteur(s) modifié(s)
|
||||
|
||||
// Identifier les secteurs concernés dans les nouvelles données
|
||||
final Set<int> sectorsToUpdate = {};
|
||||
for (final data in userSectorsList) {
|
||||
if (data['fk_sector'] != null) {
|
||||
final fkSector = data['fk_sector'] is String
|
||||
? int.parse(data['fk_sector'])
|
||||
: data['fk_sector'] as int;
|
||||
sectorsToUpdate.add(fkSector);
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer uniquement les associations des secteurs concernés
|
||||
if (sectorsToUpdate.isNotEmpty) {
|
||||
debugPrint('🗑️ Suppression des associations pour les secteurs: $sectorsToUpdate');
|
||||
final keysToDelete = <dynamic>[];
|
||||
|
||||
for (var i = 0; i < _userSectorBox.length; i++) {
|
||||
final key = _userSectorBox.keyAt(i);
|
||||
final userSector = _userSectorBox.getAt(i);
|
||||
if (userSector != null && sectorsToUpdate.contains(userSector.fkSector)) {
|
||||
keysToDelete.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (final key in keysToDelete) {
|
||||
await _userSectorBox.delete(key);
|
||||
}
|
||||
|
||||
debugPrint('📦 ${keysToDelete.length} associations supprimées');
|
||||
}
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (final userSectorData in userSectorsList) {
|
||||
try {
|
||||
final userSector = UserSectorModel.fromJson(userSectorData);
|
||||
await _userSectorBox.put('${userSector.id}_${userSector.fkSector}', userSector);
|
||||
final key = '${userSector.id}_${userSector.fkSector}';
|
||||
await _userSectorBox.put(key, userSector);
|
||||
debugPrint('✅ Association sauvegardée: ${userSector.firstName} ${userSector.name} (ID: ${userSector.id}) -> Secteur ${userSector.fkSector}');
|
||||
count++;
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ Erreur traitement association: $e');
|
||||
debugPrint('⚠️ Données problématiques: $userSectorData');
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ $count associations stockées');
|
||||
|
||||
// Vérifier le contenu de la box après sauvegarde
|
||||
debugPrint('📦 Contenu de la box UserSector après sauvegarde:');
|
||||
debugPrint(' - Nombre d\'entrées: ${_userSectorBox.length}');
|
||||
for (var i = 0; i < _userSectorBox.length && i < 5; i++) {
|
||||
final key = _userSectorBox.keyAt(i);
|
||||
final value = _userSectorBox.getAt(i);
|
||||
if (value != null) {
|
||||
debugPrint(' - [$key]: ${value.firstName} ${value.name} (ID: ${value.id}) -> Secteur ${value.fkSector}');
|
||||
}
|
||||
}
|
||||
if (_userSectorBox.length > 5) {
|
||||
debugPrint(' ... et ${_userSectorBox.length - 5} autres associations');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur traitement associations: $e');
|
||||
}
|
||||
@@ -449,7 +549,8 @@ class DataLoadingService extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ Nettoyage Android terminé. $filesDeleted fichiers supprimés');
|
||||
debugPrint(
|
||||
'✅ Nettoyage Android terminé. $filesDeleted fichiers supprimés');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur nettoyage Android: $e');
|
||||
}
|
||||
|
||||
0
app/lib/core/services/hive_adapters.dart
Normal file → Executable file
0
app/lib/core/services/hive_adapters.dart
Normal file → Executable file
32
app/lib/core/services/hive_reset_service.dart
Normal file → Executable file
32
app/lib/core/services/hive_reset_service.dart
Normal file → Executable file
@@ -4,7 +4,6 @@ import 'package:hive_flutter/hive_flutter.dart';
|
||||
// Importations conditionnelles pour le web vs non-web
|
||||
import 'js_interface.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/core/services/hive_web_fix.dart';
|
||||
import 'package:geosector_app/core/data/models/user_model.dart';
|
||||
import 'package:geosector_app/core/data/models/amicale_model.dart';
|
||||
import 'package:geosector_app/core/data/models/client_model.dart';
|
||||
@@ -22,7 +21,8 @@ class HiveResetService {
|
||||
/// Réinitialise complètement Hive et recrée les boîtes nécessaires
|
||||
static Future<bool> resetAndRecreateHiveBoxes() async {
|
||||
try {
|
||||
debugPrint('HiveResetService: Début de la réinitialisation complète de Hive');
|
||||
debugPrint(
|
||||
'HiveResetService: Début de la réinitialisation complète de Hive');
|
||||
|
||||
// Approche plus radicale pour le web : supprimer directement IndexedDB
|
||||
if (kIsWeb) {
|
||||
@@ -67,7 +67,8 @@ class HiveResetService {
|
||||
// Rouvrir les boîtes essentielles
|
||||
await _reopenEssentialBoxes();
|
||||
|
||||
debugPrint('HiveResetService: Réinitialisation complète terminée avec succès');
|
||||
debugPrint(
|
||||
'HiveResetService: Réinitialisation complète terminée avec succès');
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('HiveResetService: Erreur lors de la réinitialisation: $e');
|
||||
@@ -75,31 +76,6 @@ class HiveResetService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ferme toutes les boîtes Hive ouvertes
|
||||
static Future<void> _closeAllBoxes() async {
|
||||
final boxNames = [
|
||||
AppKeys.userBoxName,
|
||||
AppKeys.amicaleBoxName,
|
||||
AppKeys.clientsBoxName,
|
||||
AppKeys.operationsBoxName,
|
||||
AppKeys.sectorsBoxName,
|
||||
AppKeys.passagesBoxName,
|
||||
AppKeys.settingsBoxName,
|
||||
AppKeys.membresBoxName,
|
||||
AppKeys.userSectorBoxName,
|
||||
AppKeys.chatConversationsBoxName,
|
||||
AppKeys.chatMessagesBoxName,
|
||||
AppKeys.regionsBoxName,
|
||||
];
|
||||
|
||||
for (final boxName in boxNames) {
|
||||
if (Hive.isBoxOpen(boxName)) {
|
||||
debugPrint('HiveResetService: Fermeture de la boîte $boxName');
|
||||
await Hive.box(boxName).close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enregistre tous les adaptateurs Hive
|
||||
static void _registerAdapters() {
|
||||
debugPrint('HiveResetService: Enregistrement des adaptateurs Hive');
|
||||
|
||||
0
app/lib/core/services/hive_reset_state_service.dart
Normal file → Executable file
0
app/lib/core/services/hive_reset_state_service.dart
Normal file → Executable file
1
app/lib/core/services/hive_service.dart
Normal file → Executable file
1
app/lib/core/services/hive_service.dart
Normal file → Executable file
@@ -426,6 +426,7 @@ class HiveService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === MÉTHODES UTILITAIRES ===
|
||||
|
||||
/// Vider une Box individuelle
|
||||
|
||||
3
app/lib/core/services/hive_web_fix.dart
Normal file → Executable file
3
app/lib/core/services/hive_web_fix.dart
Normal file → Executable file
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:js' as js;
|
||||
import 'package:geosector_app/core/services/js_stub.dart'
|
||||
if (dart.library.js) 'dart:js' as js;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
|
||||
0
app/lib/core/services/js_interface.dart
Normal file → Executable file
0
app/lib/core/services/js_interface.dart
Normal file → Executable file
5
app/lib/core/services/js_stub.dart
Normal file → Executable file
5
app/lib/core/services/js_stub.dart
Normal file → Executable file
@@ -5,6 +5,11 @@ class JsContext {
|
||||
// Ne fait rien sur les plateformes non-web
|
||||
return null;
|
||||
}
|
||||
|
||||
bool hasProperty(String property) {
|
||||
// Retourne false sur les plateformes non-web
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Contexte JavaScript stub
|
||||
|
||||
0
app/lib/core/services/location_service.dart
Normal file → Executable file
0
app/lib/core/services/location_service.dart
Normal file → Executable file
19
app/lib/core/services/passage_data_service.dart
Normal file → Executable file
19
app/lib/core/services/passage_data_service.dart
Normal file → Executable file
@@ -48,9 +48,15 @@ class PassageDataService {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Trouver la date du passage le plus récent
|
||||
passagesToUse.sort((a, b) => b.passedAt.compareTo(a.passedAt));
|
||||
final DateTime referenceDate = passagesToUse.first.passedAt;
|
||||
// Trouver la date du passage le plus récent (exclure ceux sans date)
|
||||
final passagesWithDate = passagesToUse.where((p) => p.passedAt != null).toList();
|
||||
if (passagesWithDate.isEmpty) {
|
||||
debugPrint('Aucun passage avec date trouvé');
|
||||
return [];
|
||||
}
|
||||
|
||||
passagesWithDate.sort((a, b) => b.passedAt!.compareTo(a.passedAt!));
|
||||
final DateTime referenceDate = passagesWithDate.first.passedAt!;
|
||||
debugPrint(
|
||||
'Date de référence pour le graphique: ${DateFormat('dd/MM/yyyy').format(referenceDate)}');
|
||||
|
||||
@@ -84,12 +90,13 @@ class PassageDataService {
|
||||
|
||||
// Parcourir les passages et les regrouper par date et type
|
||||
for (final passage in passagesToUse) {
|
||||
if (passage.passedAt
|
||||
if (passage.passedAt != null &&
|
||||
passage.passedAt!
|
||||
.isAfter(startDate.subtract(const Duration(days: 1))) &&
|
||||
passage.passedAt
|
||||
passage.passedAt!
|
||||
.isBefore(referenceDate.add(const Duration(days: 1)))) {
|
||||
final dateStr =
|
||||
'${passage.passedAt.year}-${passage.passedAt.month.toString().padLeft(2, '0')}-${passage.passedAt.day.toString().padLeft(2, '0')}';
|
||||
'${passage.passedAt!.year}-${passage.passedAt!.month.toString().padLeft(2, '0')}-${passage.passedAt!.day.toString().padLeft(2, '0')}';
|
||||
final typeId = passage.fkType;
|
||||
|
||||
// Vérifier que le type n'est pas exclu
|
||||
|
||||
0
app/lib/core/services/sync_service.dart
Normal file → Executable file
0
app/lib/core/services/sync_service.dart
Normal file → Executable file
161
app/lib/core/services/theme_service.dart
Executable file
161
app/lib/core/services/theme_service.dart
Executable file
@@ -0,0 +1,161 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Service pour gérer les préférences de thème de l'application
|
||||
/// Supporte la détection automatique du mode sombre/clair du système
|
||||
class ThemeService extends ChangeNotifier {
|
||||
static ThemeService? _instance;
|
||||
static ThemeService get instance => _instance ??= ThemeService._();
|
||||
|
||||
ThemeService._() {
|
||||
_init();
|
||||
}
|
||||
|
||||
// Préférences stockées
|
||||
SharedPreferences? _prefs;
|
||||
|
||||
// Mode de thème actuel
|
||||
ThemeMode _themeMode = ThemeMode.system;
|
||||
|
||||
// Clé pour stocker les préférences
|
||||
static const String _themeModeKey = 'theme_mode';
|
||||
|
||||
/// Mode de thème actuel
|
||||
ThemeMode get themeMode => _themeMode;
|
||||
|
||||
/// Détecte si le système est en mode sombre
|
||||
bool get isSystemDark {
|
||||
final brightness = SchedulerBinding.instance.platformDispatcher.platformBrightness;
|
||||
return brightness == Brightness.dark;
|
||||
}
|
||||
|
||||
/// Détermine si l'app doit utiliser le mode sombre
|
||||
bool get isDarkMode {
|
||||
switch (_themeMode) {
|
||||
case ThemeMode.light:
|
||||
return false;
|
||||
case ThemeMode.dark:
|
||||
return true;
|
||||
case ThemeMode.system:
|
||||
return isSystemDark;
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialise le service
|
||||
Future<void> _init() async {
|
||||
try {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
await _loadThemeMode();
|
||||
|
||||
// Observer les changements du système
|
||||
SchedulerBinding.instance.platformDispatcher.onPlatformBrightnessChanged = () {
|
||||
_onSystemBrightnessChanged();
|
||||
};
|
||||
|
||||
debugPrint('🎨 ThemeService initialisé - Mode: $_themeMode, Système sombre: $isSystemDark');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur initialisation ThemeService: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Charge le mode de thème depuis les préférences
|
||||
Future<void> _loadThemeMode() async {
|
||||
try {
|
||||
final savedMode = _prefs?.getString(_themeModeKey);
|
||||
if (savedMode != null) {
|
||||
_themeMode = ThemeMode.values.firstWhere(
|
||||
(mode) => mode.name == savedMode,
|
||||
orElse: () => ThemeMode.system,
|
||||
);
|
||||
}
|
||||
debugPrint('🎨 Mode de thème chargé: $_themeMode');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur chargement thème: $e');
|
||||
_themeMode = ThemeMode.system;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sauvegarde le mode de thème
|
||||
Future<void> _saveThemeMode() async {
|
||||
try {
|
||||
await _prefs?.setString(_themeModeKey, _themeMode.name);
|
||||
debugPrint('💾 Mode de thème sauvegardé: $_themeMode');
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur sauvegarde thème: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Appelée quand la luminosité du système change
|
||||
void _onSystemBrightnessChanged() {
|
||||
if (_themeMode == ThemeMode.system) {
|
||||
debugPrint('🌗 Changement luminosité système détecté - Sombre: $isSystemDark');
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Change le mode de thème
|
||||
Future<void> setThemeMode(ThemeMode mode) async {
|
||||
if (_themeMode != mode) {
|
||||
_themeMode = mode;
|
||||
await _saveThemeMode();
|
||||
notifyListeners();
|
||||
debugPrint('🎨 Mode de thème changé: $mode');
|
||||
}
|
||||
}
|
||||
|
||||
/// Basculer entre clair et sombre
|
||||
Future<void> toggleTheme() async {
|
||||
switch (_themeMode) {
|
||||
case ThemeMode.light:
|
||||
await setThemeMode(ThemeMode.dark);
|
||||
break;
|
||||
case ThemeMode.dark:
|
||||
await setThemeMode(ThemeMode.light);
|
||||
break;
|
||||
case ThemeMode.system:
|
||||
// Si système, basculer vers l'opposé du mode actuel du système
|
||||
await setThemeMode(isSystemDark ? ThemeMode.light : ThemeMode.dark);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Retourner au mode système
|
||||
Future<void> useSystemTheme() async {
|
||||
await setThemeMode(ThemeMode.system);
|
||||
}
|
||||
|
||||
/// Forcer le mode clair
|
||||
Future<void> useLightTheme() async {
|
||||
await setThemeMode(ThemeMode.light);
|
||||
}
|
||||
|
||||
/// Forcer le mode sombre
|
||||
Future<void> useDarkTheme() async {
|
||||
await setThemeMode(ThemeMode.dark);
|
||||
}
|
||||
|
||||
/// Obtenir une description textuelle du mode actuel
|
||||
String get themeModeDescription {
|
||||
switch (_themeMode) {
|
||||
case ThemeMode.light:
|
||||
return 'Clair';
|
||||
case ThemeMode.dark:
|
||||
return 'Sombre';
|
||||
case ThemeMode.system:
|
||||
return 'Automatique (${isSystemDark ? 'sombre' : 'clair'})';
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtenir l'icône appropriée pour le mode actuel
|
||||
IconData get themeModeIcon {
|
||||
switch (_themeMode) {
|
||||
case ThemeMode.light:
|
||||
return Icons.light_mode;
|
||||
case ThemeMode.dark:
|
||||
return Icons.dark_mode;
|
||||
case ThemeMode.system:
|
||||
return Icons.brightness_auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
62
app/lib/core/theme/app_theme.dart
Normal file → Executable file
62
app/lib/core/theme/app_theme.dart
Normal file → Executable file
@@ -7,19 +7,20 @@ class AppTheme {
|
||||
static const Color accentColor = Color(0xFF00E09D); // Vert
|
||||
static const Color errorColor = Color(0xFFE41B13); // Rouge
|
||||
static const Color warningColor = Color(0xFFF7A278); // Orange
|
||||
static const Color backgroundLightColor = Color(0xFFF4F5F6); // Gris très clair
|
||||
static const Color backgroundLightColor =
|
||||
Color(0xFFF4F5F6); // Gris très clair
|
||||
static const Color backgroundDarkColor = Color(0xFF111827);
|
||||
static const Color textLightColor = Color(0xFF000000); // Noir
|
||||
static const Color textDarkColor = Color(0xFFF9FAFB);
|
||||
|
||||
|
||||
// Couleurs de texte supplémentaires
|
||||
static const Color textSecondaryColor = Color(0xFF7F8C8D);
|
||||
static const Color textLightSecondaryColor = Color(0xFFBDC3C7);
|
||||
|
||||
|
||||
// Couleurs des boutons
|
||||
static const Color buttonSuccessColor = Color(0xFF2ECC71);
|
||||
static const Color buttonDangerColor = Color(0xFFE74C3C);
|
||||
|
||||
|
||||
// Couleurs des charts
|
||||
static const List<Color> chartColors = [
|
||||
primaryColor,
|
||||
@@ -30,7 +31,7 @@ class AppTheme {
|
||||
Color(0xFF9B59B6),
|
||||
Color(0xFF1ABC9C),
|
||||
];
|
||||
|
||||
|
||||
// Ombres
|
||||
static List<BoxShadow> cardShadow = [
|
||||
BoxShadow(
|
||||
@@ -40,7 +41,7 @@ class AppTheme {
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
static List<BoxShadow> buttonShadow = [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
@@ -49,14 +50,14 @@ class AppTheme {
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
// Rayons des bordures
|
||||
static const double borderRadiusSmall = 4.0;
|
||||
static const double borderRadiusMedium = 8.0;
|
||||
static const double borderRadiusLarge = 12.0;
|
||||
static const double borderRadiusXL = 16.0;
|
||||
static const double borderRadiusRounded = 50.0;
|
||||
|
||||
|
||||
// Espacement
|
||||
static const double spacingXS = 4.0;
|
||||
static const double spacingS = 8.0;
|
||||
@@ -71,15 +72,13 @@ class AppTheme {
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.light,
|
||||
fontFamily: 'Figtree',
|
||||
colorScheme: ColorScheme.light(
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: primaryColor,
|
||||
secondary: secondaryColor,
|
||||
tertiary: accentColor,
|
||||
background: backgroundLightColor,
|
||||
surface: Colors.white,
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Colors.white,
|
||||
onBackground: textLightColor,
|
||||
onSurface: textLightColor,
|
||||
error: errorColor,
|
||||
),
|
||||
@@ -94,7 +93,8 @@ class AppTheme {
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: spacingL, vertical: spacingM),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingL, vertical: spacingM),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusRounded),
|
||||
@@ -110,7 +110,8 @@ class AppTheme {
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: primaryColor,
|
||||
side: const BorderSide(color: primaryColor),
|
||||
padding: const EdgeInsets.symmetric(horizontal: spacingL, vertical: spacingM),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingL, vertical: spacingM),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
||||
),
|
||||
@@ -119,7 +120,8 @@ class AppTheme {
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: primaryColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: spacingM, vertical: spacingS),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingM, vertical: spacingS),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
@@ -143,7 +145,8 @@ class AppTheme {
|
||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
||||
borderSide: const BorderSide(color: primaryColor, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: spacingM, vertical: spacingM),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingM, vertical: spacingM),
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 2,
|
||||
@@ -166,15 +169,13 @@ class AppTheme {
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
fontFamily: 'Figtree',
|
||||
colorScheme: ColorScheme.dark(
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: primaryColor,
|
||||
secondary: secondaryColor,
|
||||
tertiary: accentColor,
|
||||
background: backgroundDarkColor,
|
||||
surface: const Color(0xFF1F2937),
|
||||
surface: Color(0xFF1F2937),
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Colors.white,
|
||||
onBackground: textDarkColor,
|
||||
onSurface: textDarkColor,
|
||||
error: errorColor,
|
||||
),
|
||||
@@ -189,7 +190,8 @@ class AppTheme {
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: spacingL, vertical: spacingM),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingL, vertical: spacingM),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusRounded),
|
||||
@@ -205,7 +207,8 @@ class AppTheme {
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: primaryColor,
|
||||
side: const BorderSide(color: primaryColor),
|
||||
padding: const EdgeInsets.symmetric(horizontal: spacingL, vertical: spacingM),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingL, vertical: spacingM),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
||||
),
|
||||
@@ -214,7 +217,8 @@ class AppTheme {
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: primaryColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: spacingM, vertical: spacingS),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingM, vertical: spacingS),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
@@ -238,7 +242,8 @@ class AppTheme {
|
||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
||||
borderSide: const BorderSide(color: primaryColor, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: spacingM, vertical: spacingM),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: spacingM, vertical: spacingM),
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 4,
|
||||
@@ -269,10 +274,13 @@ class AppTheme {
|
||||
titleSmall: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
bodyLarge: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
bodyMedium: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
bodySmall: TextStyle(fontFamily: 'Figtree', color: textColor.withOpacity(0.7)),
|
||||
bodySmall:
|
||||
TextStyle(fontFamily: 'Figtree', color: textColor.withOpacity(0.7)),
|
||||
labelLarge: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
labelMedium: TextStyle(fontFamily: 'Figtree', color: textColor.withOpacity(0.7)),
|
||||
labelSmall: TextStyle(fontFamily: 'Figtree', color: textColor.withOpacity(0.7)),
|
||||
labelMedium:
|
||||
TextStyle(fontFamily: 'Figtree', color: textColor.withOpacity(0.7)),
|
||||
labelSmall:
|
||||
TextStyle(fontFamily: 'Figtree', color: textColor.withOpacity(0.7)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0
app/lib/core/utils/api_exception.dart
Normal file → Executable file
0
app/lib/core/utils/api_exception.dart
Normal file → Executable file
0
app/lib/main.dart
Normal file → Executable file
0
app/lib/main.dart
Normal file → Executable file
0
app/lib/presentation/MIGRATION.md
Normal file → Executable file
0
app/lib/presentation/MIGRATION.md
Normal file → Executable file
231
app/lib/presentation/admin/admin_amicale_page.dart
Normal file → Executable file
231
app/lib/presentation/admin/admin_amicale_page.dart
Normal file → Executable file
@@ -11,7 +11,6 @@ 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';
|
||||
import 'package:geosector_app/core/utils/api_exception.dart';
|
||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||
import 'package:geosector_app/core/repositories/passage_repository.dart';
|
||||
import 'package:geosector_app/core/repositories/operation_repository.dart';
|
||||
|
||||
@@ -52,7 +51,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
void _loadCurrentUser() {
|
||||
final currentUser = widget.userRepository.getCurrentUser();
|
||||
|
||||
debugPrint('🔍 _loadCurrentUser - Utilisateur: ${currentUser?.username} (ID: ${currentUser?.id})');
|
||||
debugPrint(
|
||||
'🔍 _loadCurrentUser - Utilisateur: ${currentUser?.username} (ID: ${currentUser?.id})');
|
||||
debugPrint('🔍 _loadCurrentUser - fkEntite: ${currentUser?.fkEntite}');
|
||||
|
||||
if (currentUser == null) {
|
||||
@@ -70,8 +70,10 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
}
|
||||
|
||||
// Vérifier immédiatement si l'amicale existe
|
||||
final amicale = widget.amicaleRepository.getUserAmicale(currentUser.fkEntite!);
|
||||
debugPrint('🔍 Amicale trouvée dans le repository: ${amicale?.name ?? 'null'}');
|
||||
final amicale =
|
||||
widget.amicaleRepository.getUserAmicale(currentUser.fkEntite!);
|
||||
debugPrint(
|
||||
'🔍 Amicale trouvée dans le repository: ${amicale?.name ?? 'null'}');
|
||||
|
||||
setState(() {
|
||||
_currentUser = currentUser;
|
||||
@@ -85,8 +87,10 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
_currentOperationId = currentOperation?.id;
|
||||
|
||||
if (currentOperation != null) {
|
||||
debugPrint('🎯 Opération courante: ${currentOperation.id} - ${currentOperation.name}');
|
||||
debugPrint('📅 Période: ${currentOperation.dateDebut.toString().substring(0, 10)} → ${currentOperation.dateFin.toString().substring(0, 10)}');
|
||||
debugPrint(
|
||||
'🎯 Opération courante: ${currentOperation.id} - ${currentOperation.name}');
|
||||
debugPrint(
|
||||
'📅 Période: ${currentOperation.dateDebut.toString().substring(0, 10)} → ${currentOperation.dateFin.toString().substring(0, 10)}');
|
||||
} else {
|
||||
debugPrint('⚠️ Aucune opération courante trouvée');
|
||||
}
|
||||
@@ -117,16 +121,20 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
onSubmit: (updatedUser) async {
|
||||
try {
|
||||
// Convertir le UserModel mis à jour vers MembreModel
|
||||
final updatedMembre = MembreModel.fromUserModel(updatedUser, membre);
|
||||
final updatedMembre =
|
||||
MembreModel.fromUserModel(updatedUser, membre);
|
||||
|
||||
// Utiliser directement updateMembre qui passe par l'API /users
|
||||
final success = await widget.membreRepository.updateMembre(updatedMembre);
|
||||
final success =
|
||||
await widget.membreRepository.updateMembre(updatedMembre);
|
||||
|
||||
if (success && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ApiException.showSuccess(context, 'Membre ${updatedMembre.firstName} ${updatedMembre.name} mis à jour');
|
||||
ApiException.showSuccess(context,
|
||||
'Membre ${updatedMembre.firstName} ${updatedMembre.name} mis à jour');
|
||||
} else if (!success && mounted) {
|
||||
ApiException.showError(context, Exception('Erreur lors de la mise à jour'));
|
||||
ApiException.showError(
|
||||
context, Exception('Erreur lors de la mise à jour'));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur mise à jour membre: $e');
|
||||
@@ -139,42 +147,119 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _handleResetPassword(MembreModel membre) async {
|
||||
// Afficher un dialog de confirmation
|
||||
final bool? confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.lock_reset, color: Colors.blue),
|
||||
SizedBox(width: 8),
|
||||
Text('Réinitialiser le mot de passe'),
|
||||
],
|
||||
),
|
||||
content: Text(
|
||||
'Voulez-vous réinitialiser le mot de passe de ${membre.firstName} ${membre.name} ?\n\n'
|
||||
'Un email sera envoyé à l\'utilisateur avec les instructions de réinitialisation.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Réinitialiser'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm != true) return;
|
||||
|
||||
try {
|
||||
debugPrint('🔐 Réinitialisation du mot de passe pour: ${membre.firstName} ${membre.name} (ID: ${membre.id})');
|
||||
|
||||
final success = await widget.membreRepository.resetMemberPassword(membre.id);
|
||||
|
||||
if (success && mounted) {
|
||||
ApiException.showSuccess(
|
||||
context,
|
||||
'Mot de passe réinitialisé avec succès. Un email a été envoyé à ${membre.email}',
|
||||
);
|
||||
} else if (mounted) {
|
||||
ApiException.showError(
|
||||
context,
|
||||
Exception('Erreur lors de la réinitialisation du mot de passe'),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur réinitialisation mot de passe: $e');
|
||||
if (mounted) {
|
||||
ApiException.showError(context, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDeleteMembre(MembreModel membre) async {
|
||||
try {
|
||||
debugPrint('🗑️ Début suppression du membre: ${membre.firstName} ${membre.name} (ID: ${membre.id})');
|
||||
debugPrint(
|
||||
'🗑️ Début suppression du membre: ${membre.firstName} ${membre.name} (ID: ${membre.id})');
|
||||
|
||||
// Vérifier qu'on a une opération courante
|
||||
if (_currentOperationId == null) {
|
||||
debugPrint('❌ Aucune opération courante');
|
||||
ApiException.showError(context, Exception('Aucune opération active trouvée. Impossible de supprimer le membre.'));
|
||||
ApiException.showError(
|
||||
context,
|
||||
Exception(
|
||||
'Aucune opération active trouvée. Impossible de supprimer le membre.'));
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('🎯 Opération courante: $_currentOperationId');
|
||||
|
||||
// Filtrer les passages par opération courante ET par utilisateur
|
||||
final allUserPassages = widget.passageRepository.getPassagesByUser(membre.id);
|
||||
final allUserPassages =
|
||||
widget.passageRepository.getPassagesByUser(membre.id);
|
||||
debugPrint('📊 Total passages du membre: ${allUserPassages.length}');
|
||||
|
||||
final passagesRealises = allUserPassages.where((passage) => passage.fkOperation == _currentOperationId && passage.fkType != 2).toList();
|
||||
final passagesRealises = allUserPassages
|
||||
.where((passage) =>
|
||||
passage.fkOperation == _currentOperationId && passage.fkType != 2)
|
||||
.toList();
|
||||
|
||||
final passagesAFinaliser = allUserPassages.where((passage) => passage.fkOperation == _currentOperationId && passage.fkType == 2).toList();
|
||||
final passagesAFinaliser = allUserPassages
|
||||
.where((passage) =>
|
||||
passage.fkOperation == _currentOperationId && passage.fkType == 2)
|
||||
.toList();
|
||||
|
||||
final totalPassages = passagesRealises.length + passagesAFinaliser.length;
|
||||
|
||||
debugPrint('🔍 Passages réalisés (opération $_currentOperationId): ${passagesRealises.length}');
|
||||
debugPrint('🔍 Passages à finaliser (opération $_currentOperationId): ${passagesAFinaliser.length}');
|
||||
debugPrint('🔍 Total passages pour l\'opération $_currentOperationId: $totalPassages');
|
||||
debugPrint(
|
||||
'🔍 Passages réalisés (opération $_currentOperationId): ${passagesRealises.length}');
|
||||
debugPrint(
|
||||
'🔍 Passages à finaliser (opération $_currentOperationId): ${passagesAFinaliser.length}');
|
||||
debugPrint(
|
||||
'🔍 Total passages pour l\'opération $_currentOperationId: $totalPassages');
|
||||
|
||||
// Récupérer les autres membres de l'amicale (pour le transfert)
|
||||
final autresmembres = widget.membreRepository.getMembresByAmicale(_currentUser!.fkEntite!).where((m) => m.id != membre.id && m.isActive == true).toList();
|
||||
final autresmembres = widget.membreRepository
|
||||
.getMembresByAmicale(_currentUser!.fkEntite!)
|
||||
.where((m) => m.id != membre.id && m.isActive == true)
|
||||
.toList();
|
||||
|
||||
debugPrint('👥 Autres membres disponibles: ${autresmembres.length}');
|
||||
|
||||
// Afficher le dialog de confirmation approprié
|
||||
if (totalPassages > 0) {
|
||||
debugPrint('➡️ Affichage dialog avec passages');
|
||||
_showDeleteMemberWithPassagesDialog(membre, totalPassages, autresmembres);
|
||||
_showDeleteMemberWithPassagesDialog(
|
||||
membre, totalPassages, autresmembres);
|
||||
} else {
|
||||
debugPrint('➡️ Affichage dialog simple (pas de passages)');
|
||||
_showSimpleDeleteConfirmation(membre);
|
||||
@@ -192,7 +277,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Confirmer la suppression'),
|
||||
content: Text('Voulez-vous vraiment supprimer le membre ${membre.firstName} ${membre.name} ?\n\n'
|
||||
content: Text(
|
||||
'Voulez-vous vraiment supprimer le membre ${membre.firstName} ${membre.name} ?\n\n'
|
||||
'Ce membre n\'a aucun passage enregistré pour l\'opération courante.\n'
|
||||
'Cette action est irréversible.'),
|
||||
actions: [
|
||||
@@ -222,7 +308,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
int totalPassages,
|
||||
List<MembreModel> autresmembres,
|
||||
) {
|
||||
int? selectedMemberForTransfer; // Déclarer la variable à l'extérieur du builder
|
||||
int?
|
||||
selectedMemberForTransfer; // Déclarer la variable à l'extérieur du builder
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -272,13 +359,19 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
Text(
|
||||
'Sélectionnez un membre pour récupérer tous les passages ($totalPassages) :',
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'* Cela peut concerner aussi les anciennes opérations s\'il avait des passages affectés',
|
||||
style: TextStyle(fontSize: 12, fontStyle: FontStyle.italic),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<int>(
|
||||
value: selectedMemberForTransfer,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Membre destinataire',
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
items: autresmembres
|
||||
.map((m) => DropdownMenuItem(
|
||||
@@ -290,7 +383,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
setDialogState(() {
|
||||
selectedMemberForTransfer = value;
|
||||
});
|
||||
debugPrint('✅ Membre destinataire sélectionné: $value');
|
||||
debugPrint(
|
||||
'✅ Membre destinataire sélectionné: $value');
|
||||
},
|
||||
),
|
||||
|
||||
@@ -305,7 +399,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.check_circle, color: Colors.green, size: 16),
|
||||
const Icon(Icons.check_circle,
|
||||
color: Colors.green, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Membre sélectionné',
|
||||
@@ -329,7 +424,8 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.green.withOpacity(0.3)),
|
||||
border:
|
||||
Border.all(color: Colors.green.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -371,23 +467,31 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
ElevatedButton(
|
||||
onPressed: selectedMemberForTransfer != null
|
||||
? () async {
|
||||
debugPrint('🗑️ Suppression avec transfert vers ID: $selectedMemberForTransfer');
|
||||
debugPrint(
|
||||
'🗑️ Suppression avec transfert vers ID: $selectedMemberForTransfer');
|
||||
Navigator.of(context).pop();
|
||||
// Suppression avec passages : inclure les paramètres
|
||||
await _deleteMemberAPI(membre.id, selectedMemberForTransfer!, hasPassages: true);
|
||||
await _deleteMemberAPI(
|
||||
membre.id, selectedMemberForTransfer!,
|
||||
hasPassages: true);
|
||||
}
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: selectedMemberForTransfer != null ? Colors.red : null,
|
||||
backgroundColor:
|
||||
selectedMemberForTransfer != null ? Colors.red : null,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (selectedMemberForTransfer != null) const Icon(Icons.delete_forever, size: 16),
|
||||
if (selectedMemberForTransfer != null) const SizedBox(width: 4),
|
||||
if (selectedMemberForTransfer != null)
|
||||
const Icon(Icons.delete_forever, size: 16),
|
||||
if (selectedMemberForTransfer != null)
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
selectedMemberForTransfer != null ? 'Supprimer et transférer' : 'Sélectionner un membre',
|
||||
selectedMemberForTransfer != null
|
||||
? 'Supprimer et transférer'
|
||||
: 'Sélectionner un membre',
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -400,13 +504,15 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
}
|
||||
|
||||
// Méthode unifiée pour appeler l'API de suppression
|
||||
Future<void> _deleteMemberAPI(int membreId, int transferToUserId, {bool hasPassages = false}) async {
|
||||
Future<void> _deleteMemberAPI(int membreId, int transferToUserId,
|
||||
{bool hasPassages = false}) async {
|
||||
try {
|
||||
bool success;
|
||||
|
||||
if (hasPassages && transferToUserId > 0 && _currentOperationId != null) {
|
||||
// Suppression avec transfert de passages (inclure operation_id)
|
||||
debugPrint('🔄 Suppression avec transfert - Opération: $_currentOperationId, Vers: $transferToUserId');
|
||||
debugPrint(
|
||||
'🔄 Suppression avec transfert - Opération: $_currentOperationId, Vers: $transferToUserId');
|
||||
success = await widget.membreRepository.deleteMembre(
|
||||
membreId,
|
||||
transferToUserId,
|
||||
@@ -422,14 +528,18 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
String message = 'Membre supprimé avec succès';
|
||||
|
||||
if (hasPassages && transferToUserId > 0) {
|
||||
final transferMember = widget.membreRepository.getMembreById(transferToUserId);
|
||||
final currentOperation = widget.operationRepository.getCurrentOperation();
|
||||
message += '\nPassages de l\'opération "${currentOperation?.name}" transférés à ${transferMember?.firstName} ${transferMember?.name}';
|
||||
final transferMember =
|
||||
widget.membreRepository.getMembreById(transferToUserId);
|
||||
final currentOperation =
|
||||
widget.operationRepository.getCurrentOperation();
|
||||
message +=
|
||||
'\nPassages de l\'opération "${currentOperation?.name}" transférés à ${transferMember?.firstName} ${transferMember?.name}';
|
||||
}
|
||||
|
||||
ApiException.showSuccess(context, message);
|
||||
} else if (mounted) {
|
||||
ApiException.showError(context, Exception('Erreur lors de la suppression'));
|
||||
ApiException.showError(
|
||||
context, Exception('Erreur lors de la suppression'));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur suppression membre: $e');
|
||||
@@ -445,9 +555,11 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
final success = await widget.membreRepository.updateMembre(updatedMember);
|
||||
|
||||
if (success && mounted) {
|
||||
ApiException.showSuccess(context, 'Membre ${membre.firstName} ${membre.name} désactivé avec succès');
|
||||
ApiException.showSuccess(context,
|
||||
'Membre ${membre.firstName} ${membre.name} désactivé avec succès');
|
||||
} else if (mounted) {
|
||||
ApiException.showError(context, Exception('Erreur lors de la désactivation'));
|
||||
ApiException.showError(
|
||||
context, Exception('Erreur lors de la désactivation'));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
@@ -519,17 +631,20 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
);
|
||||
|
||||
// Créer le membre via l'API (retourne maintenant le membre créé)
|
||||
final createdMembre = await widget.membreRepository.createMembre(newMembre);
|
||||
final createdMembre =
|
||||
await widget.membreRepository.createMembre(newMembre);
|
||||
|
||||
if (createdMembre != null && mounted) {
|
||||
// Fermer le dialog
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Afficher le message de succès avec les informations du membre créé
|
||||
ApiException.showSuccess(context, 'Membre ${createdMembre.firstName} ${createdMembre.name} ajouté avec succès (ID: ${createdMembre.id})');
|
||||
ApiException.showSuccess(context,
|
||||
'Membre ${createdMembre.firstName} ${createdMembre.name} ajouté avec succès (ID: ${createdMembre.id})');
|
||||
} else if (mounted) {
|
||||
// En cas d'échec, ne pas fermer le dialog pour permettre la correction
|
||||
ApiException.showError(context, Exception('Erreur lors de la création du membre'));
|
||||
ApiException.showError(
|
||||
context, Exception('Erreur lors de la création du membre'));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Erreur création membre: $e');
|
||||
@@ -593,20 +708,27 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
if (_currentUser != null && _currentUser!.fkEntite != null)
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<Box<AmicaleModel>>(
|
||||
valueListenable: widget.amicaleRepository.getAmicalesBox().listenable(),
|
||||
valueListenable:
|
||||
widget.amicaleRepository.getAmicalesBox().listenable(),
|
||||
builder: (context, amicalesBox, child) {
|
||||
debugPrint('🔍 AmicalesBox - Nombre d\'amicales: ${amicalesBox.length}');
|
||||
debugPrint('🔍 AmicalesBox - Clés disponibles: ${amicalesBox.keys.toList()}');
|
||||
debugPrint('🔍 Recherche amicale avec fkEntite: ${_currentUser!.fkEntite}');
|
||||
debugPrint(
|
||||
'🔍 AmicalesBox - Nombre d\'amicales: ${amicalesBox.length}');
|
||||
debugPrint(
|
||||
'🔍 AmicalesBox - Clés disponibles: ${amicalesBox.keys.toList()}');
|
||||
debugPrint(
|
||||
'🔍 Recherche amicale avec fkEntite: ${_currentUser!.fkEntite}');
|
||||
|
||||
final amicale = amicalesBox.get(_currentUser!.fkEntite!);
|
||||
debugPrint('🔍 Amicale récupérée: ${amicale?.name ?? 'AUCUNE'}');
|
||||
debugPrint(
|
||||
'🔍 Amicale récupérée: ${amicale?.name ?? 'AUCUNE'}');
|
||||
|
||||
if (amicale == null) {
|
||||
// Ajouter plus d'informations de debug
|
||||
debugPrint('❌ PROBLÈME: Amicale non trouvée');
|
||||
debugPrint('❌ fkEntite recherché: ${_currentUser!.fkEntite}');
|
||||
debugPrint('❌ Contenu de la box: ${amicalesBox.values.map((a) => '${a.id}: ${a.name}').join(', ')}');
|
||||
debugPrint(
|
||||
'❌ fkEntite recherché: ${_currentUser!.fkEntite}');
|
||||
debugPrint(
|
||||
'❌ Contenu de la box: ${amicalesBox.values.map((a) => '${a.id}: ${a.name}').join(', ')}');
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
@@ -634,11 +756,15 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
}
|
||||
|
||||
return ValueListenableBuilder<Box<MembreModel>>(
|
||||
valueListenable: widget.membreRepository.getMembresBox().listenable(),
|
||||
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();
|
||||
final membres = membresBox.values
|
||||
.where((membre) =>
|
||||
membre.fkEntite == _currentUser!.fkEntite)
|
||||
.toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -721,6 +847,7 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
|
||||
membres: membres,
|
||||
onEdit: _handleEditMembre,
|
||||
onDelete: _handleDeleteMembre,
|
||||
onResetPassword: _handleResetPassword,
|
||||
membreRepository: widget.membreRepository,
|
||||
),
|
||||
),
|
||||
|
||||
2
app/lib/presentation/admin/admin_communication_page.dart
Normal file → Executable file
2
app/lib/presentation/admin/admin_communication_page.dart
Normal file → Executable file
@@ -5,7 +5,7 @@ import 'package:geosector_app/presentation/widgets/chat/chat_messages.dart';
|
||||
import 'package:geosector_app/presentation/widgets/chat/chat_input.dart';
|
||||
|
||||
class AdminCommunicationPage extends StatefulWidget {
|
||||
const AdminCommunicationPage({Key? key}) : super(key: key);
|
||||
const AdminCommunicationPage({super.key});
|
||||
|
||||
@override
|
||||
State<AdminCommunicationPage> createState() => _AdminCommunicationPageState();
|
||||
|
||||
0
app/lib/presentation/admin/admin_dashboard_home_page.dart
Normal file → Executable file
0
app/lib/presentation/admin/admin_dashboard_home_page.dart
Normal file → Executable file
57
app/lib/presentation/admin/admin_dashboard_page.dart
Normal file → Executable file
57
app/lib/presentation/admin/admin_dashboard_page.dart
Normal file → Executable file
@@ -47,8 +47,7 @@ class AdminDashboardPage extends StatefulWidget {
|
||||
class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBindingObserver {
|
||||
int _selectedIndex = 0;
|
||||
|
||||
// Liste des pages à afficher
|
||||
late final List<Widget> _pages;
|
||||
// Pages seront construites dynamiquement dans build()
|
||||
|
||||
// Référence à la boîte Hive pour les paramètres
|
||||
late Box _settingsBox;
|
||||
@@ -138,6 +137,8 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
List<NavigationDestination> _buildNavigationDestinations() {
|
||||
final destinations = <NavigationDestination>[];
|
||||
final currentUser = userRepository.getCurrentUser();
|
||||
final size = MediaQuery.of(context).size;
|
||||
final isMobile = size.width <= 900;
|
||||
|
||||
// Ajouter les éléments de base
|
||||
for (final item in _baseNavigationItems) {
|
||||
@@ -153,6 +154,12 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
// Ajouter les éléments admin si l'utilisateur a le rôle requis
|
||||
if (currentUser?.role == 2) {
|
||||
for (final item in _adminNavigationItems) {
|
||||
// En mobile, exclure "Amicale & membres" et "Opérations"
|
||||
if (isMobile &&
|
||||
(item.label == 'Amicale & membres' || item.label == 'Opérations')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.requiredRole == null || item.requiredRole == 2) {
|
||||
destinations.add(
|
||||
NavigationDestination(
|
||||
@@ -172,6 +179,8 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
List<Widget> _buildPages() {
|
||||
final pages = <Widget>[];
|
||||
final currentUser = userRepository.getCurrentUser();
|
||||
final size = MediaQuery.of(context).size;
|
||||
final isMobile = size.width <= 900;
|
||||
|
||||
// Ajouter les pages de base
|
||||
for (final item in _baseNavigationItems) {
|
||||
@@ -181,6 +190,12 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
// Ajouter les pages admin si l'utilisateur a le rôle requis
|
||||
if (currentUser?.role == 2) {
|
||||
for (final item in _adminNavigationItems) {
|
||||
// En mobile, exclure "Amicale & membres" et "Opérations"
|
||||
if (isMobile &&
|
||||
(item.label == 'Amicale & membres' || item.label == 'Opérations')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.requiredRole == null || item.requiredRole == 2) {
|
||||
pages.add(_buildPage(item.pageType));
|
||||
}
|
||||
@@ -208,8 +223,7 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
}
|
||||
userRepository.addListener(_handleUserRepositoryChanges);
|
||||
|
||||
// Initialiser les pages et les destinations
|
||||
_pages = _buildPages();
|
||||
// Les pages seront construites dynamiquement dans build()
|
||||
|
||||
// Initialiser et charger les paramètres
|
||||
_initSettings();
|
||||
@@ -257,19 +271,11 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
if (savedIndex != null && savedIndex is int) {
|
||||
debugPrint('Index sauvegardé trouvé: $savedIndex');
|
||||
|
||||
// S'assurer que l'index est dans les limites valides
|
||||
if (savedIndex >= 0 && savedIndex < _pages.length) {
|
||||
setState(() {
|
||||
_selectedIndex = savedIndex;
|
||||
});
|
||||
debugPrint('Index sauvegardé valide, utilisé: $_selectedIndex');
|
||||
} else {
|
||||
debugPrint(
|
||||
'Index sauvegardé invalide ($savedIndex), utilisation de l\'index par défaut: 0',
|
||||
);
|
||||
// Réinitialiser l'index sauvegardé à 0 si invalide
|
||||
_settingsBox.put('adminSelectedPageIndex', 0);
|
||||
}
|
||||
// La validation de l'index sera faite dans build()
|
||||
setState(() {
|
||||
_selectedIndex = savedIndex;
|
||||
});
|
||||
debugPrint('Index sauvegardé utilisé: $_selectedIndex');
|
||||
} else {
|
||||
debugPrint(
|
||||
'Aucun index sauvegardé trouvé, utilisation de l\'index par défaut: 0',
|
||||
@@ -292,6 +298,19 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Construire les pages et destinations dynamiquement
|
||||
final pages = _buildPages();
|
||||
final destinations = _buildNavigationDestinations();
|
||||
|
||||
// Valider et ajuster l'index si nécessaire
|
||||
if (_selectedIndex >= pages.length) {
|
||||
_selectedIndex = 0;
|
||||
// Sauvegarder le nouvel index
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_saveSettings();
|
||||
});
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
// Fond dégradé avec petits points blancs
|
||||
@@ -318,10 +337,10 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
_saveSettings(); // Sauvegarder l'index de page sélectionné
|
||||
});
|
||||
},
|
||||
destinations: _buildNavigationDestinations(),
|
||||
destinations: destinations,
|
||||
showNewPassageButton: false,
|
||||
isAdmin: true,
|
||||
body: _pages[_selectedIndex],
|
||||
body: pages[_selectedIndex],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
5
app/lib/presentation/admin/admin_debug_info_widget.dart
Normal file → Executable file
5
app/lib/presentation/admin/admin_debug_info_widget.dart
Normal file → Executable file
@@ -4,7 +4,7 @@ import 'package:geosector_app/presentation/widgets/environment_info_widget.dart'
|
||||
/// Widget d'information de débogage pour l'administrateur
|
||||
/// À intégrer où nécessaire dans l'interface administrateur
|
||||
class AdminDebugInfoWidget extends StatelessWidget {
|
||||
const AdminDebugInfoWidget({Key? key}) : super(key: key);
|
||||
const AdminDebugInfoWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -30,7 +30,8 @@ class AdminDebugInfoWidget extends StatelessWidget {
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('Environnement'),
|
||||
subtitle: const Text('Afficher les informations sur l\'environnement actuel'),
|
||||
subtitle: const Text(
|
||||
'Afficher les informations sur l\'environnement actuel'),
|
||||
onTap: () => EnvironmentInfoWidget.show(context),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
|
||||
715
app/lib/presentation/admin/admin_history_page.dart
Normal file → Executable file
715
app/lib/presentation/admin/admin_history_page.dart
Normal file → Executable file
@@ -1,15 +1,15 @@
|
||||
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:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||
import 'package:geosector_app/core/data/models/sector_model.dart';
|
||||
import 'package:geosector_app/core/data/models/user_model.dart';
|
||||
import 'package:geosector_app/core/data/models/membre_model.dart';
|
||||
import 'package:geosector_app/core/repositories/passage_repository.dart';
|
||||
import 'package:geosector_app/core/repositories/sector_repository.dart';
|
||||
import 'package:geosector_app/core/repositories/user_repository.dart';
|
||||
import 'package:geosector_app/core/theme/app_theme.dart';
|
||||
import 'package:geosector_app/core/repositories/membre_repository.dart';
|
||||
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
|
||||
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
/// Class pour dessiner les petits points blancs sur le fond
|
||||
@@ -36,7 +36,7 @@ class DotsPainter extends CustomPainter {
|
||||
}
|
||||
|
||||
class AdminHistoryPage extends StatefulWidget {
|
||||
const AdminHistoryPage({Key? key}) : super(key: key);
|
||||
const AdminHistoryPage({super.key});
|
||||
|
||||
@override
|
||||
State<AdminHistoryPage> createState() => _AdminHistoryPageState();
|
||||
@@ -49,25 +49,32 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
String selectedUser = 'Tous';
|
||||
String selectedType = 'Tous';
|
||||
String selectedPaymentMethod = 'Tous';
|
||||
String selectedPeriod = 'Dernier mois'; // Période par défaut
|
||||
String selectedPeriod = 'Tous'; // Période par défaut
|
||||
DateTimeRange? selectedDateRange;
|
||||
|
||||
// Contrôleur pour la recherche
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
|
||||
// IDs pour les filtres
|
||||
int? selectedSectorId;
|
||||
int? selectedUserId;
|
||||
|
||||
// Listes pour les filtres
|
||||
List<SectorModel> _sectors = [];
|
||||
List<UserModel> _users = [];
|
||||
List<MembreModel> _membres = [];
|
||||
|
||||
// Repositories
|
||||
late PassageRepository _passageRepository;
|
||||
late SectorRepository _sectorRepository;
|
||||
late UserRepository _userRepository;
|
||||
late MembreRepository _membreRepository;
|
||||
|
||||
// Passages formatés
|
||||
// Passages formatés pour l'affichage
|
||||
List<Map<String, dynamic>> _formattedPassages = [];
|
||||
|
||||
// Passages originaux pour l'édition
|
||||
List<PassageModel> _originalPassages = [];
|
||||
|
||||
// État de chargement
|
||||
bool _isLoading = true;
|
||||
String _errorMessage = '';
|
||||
@@ -93,9 +100,10 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
_passageRepository = passageRepository;
|
||||
_userRepository = userRepository;
|
||||
_sectorRepository = sectorRepository;
|
||||
_membreRepository = membreRepository;
|
||||
|
||||
// Charger les secteurs et les utilisateurs
|
||||
_loadSectorsAndUsers();
|
||||
// Charger les secteurs et les membres
|
||||
_loadSectorsAndMembres();
|
||||
|
||||
// Charger les passages
|
||||
_loadPassages();
|
||||
@@ -107,18 +115,18 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
}
|
||||
}
|
||||
|
||||
// Charger les secteurs et les utilisateurs
|
||||
void _loadSectorsAndUsers() {
|
||||
// Charger les secteurs et les membres
|
||||
void _loadSectorsAndMembres() {
|
||||
try {
|
||||
// Récupérer la liste des secteurs
|
||||
_sectors = _sectorRepository.getAllSectors();
|
||||
debugPrint('Nombre de secteurs récupérés: ${_sectors.length}');
|
||||
|
||||
// Récupérer la liste des utilisateurs
|
||||
_users = _userRepository.getAllUsers();
|
||||
debugPrint('Nombre d\'utilisateurs récupérés: ${_users.length}');
|
||||
// Récupérer la liste des membres
|
||||
_membres = _membreRepository.getAllMembres();
|
||||
debugPrint('Nombre de membres récupérés: ${_membres.length}');
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du chargement des secteurs et utilisateurs: $e');
|
||||
debugPrint('Erreur lors du chargement des secteurs et membres: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,9 +141,12 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
final List<PassageModel> allPassages =
|
||||
_passageRepository.getAllPassages();
|
||||
|
||||
// Stocker les passages originaux pour l'édition
|
||||
_originalPassages = allPassages;
|
||||
|
||||
// Convertir les passages en format attendu par PassagesListWidget
|
||||
_formattedPassages = _formatPassagesForWidget(
|
||||
allPassages, _sectorRepository, _userRepository);
|
||||
allPassages, _sectorRepository, _membreRepository);
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
@@ -154,13 +165,137 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
selectedSectorId = null;
|
||||
selectedUserId = null;
|
||||
|
||||
// Période par défaut : dernier mois
|
||||
selectedPeriod = 'Dernier mois';
|
||||
// Période par défaut : toutes les périodes
|
||||
selectedPeriod = 'Tous';
|
||||
|
||||
// Plage de dates par défaut : dernier mois
|
||||
final DateTime now = DateTime.now();
|
||||
final DateTime oneMonthAgo = DateTime(now.year, now.month - 1, now.day);
|
||||
selectedDateRange = DateTimeRange(start: oneMonthAgo, end: now);
|
||||
// Plage de dates par défaut : aucune restriction
|
||||
selectedDateRange = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Méthode pour appliquer tous les filtres
|
||||
List<Map<String, dynamic>> _getFilteredPassages() {
|
||||
try {
|
||||
var filtered = _formattedPassages.where((passage) {
|
||||
try {
|
||||
// Ne plus exclure automatiquement les passages de type 2
|
||||
// car on propose maintenant un filtre par type dans les "Filtres avancés"
|
||||
|
||||
// Filtrer par utilisateur
|
||||
if (selectedUserId != null &&
|
||||
passage.containsKey('fkUser') &&
|
||||
passage['fkUser'] != selectedUserId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filtrer par secteur
|
||||
if (selectedSectorId != null &&
|
||||
passage.containsKey('fkSector') &&
|
||||
passage['fkSector'] != selectedSectorId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filtrer par type de passage
|
||||
if (selectedType != 'Tous') {
|
||||
try {
|
||||
final int? selectedTypeId = int.tryParse(selectedType);
|
||||
if (selectedTypeId != null) {
|
||||
if (!passage.containsKey('type') ||
|
||||
passage['type'] != selectedTypeId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur de filtrage par type: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par mode de règlement
|
||||
if (selectedPaymentMethod != 'Tous') {
|
||||
try {
|
||||
final int? selectedPaymentId =
|
||||
int.tryParse(selectedPaymentMethod);
|
||||
if (selectedPaymentId != null) {
|
||||
if (!passage.containsKey('payment') ||
|
||||
passage['payment'] != selectedPaymentId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur de filtrage par mode de règlement: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par recherche
|
||||
if (searchQuery.isNotEmpty) {
|
||||
try {
|
||||
final query = searchQuery.toLowerCase();
|
||||
final address = passage.containsKey('address')
|
||||
? passage['address']?.toString().toLowerCase() ?? ''
|
||||
: '';
|
||||
final name = passage.containsKey('name')
|
||||
? passage['name']?.toString().toLowerCase() ?? ''
|
||||
: '';
|
||||
final notes = passage.containsKey('notes')
|
||||
? passage['notes']?.toString().toLowerCase() ?? ''
|
||||
: '';
|
||||
|
||||
if (!address.contains(query) &&
|
||||
!name.contains(query) &&
|
||||
!notes.contains(query)) {
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur de filtrage par recherche: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par période/date
|
||||
if (selectedDateRange != null) {
|
||||
try {
|
||||
if (passage.containsKey('date') && passage['date'] is DateTime) {
|
||||
final DateTime passageDate = passage['date'] as DateTime;
|
||||
if (passageDate.isBefore(selectedDateRange!.start) ||
|
||||
passageDate.isAfter(selectedDateRange!.end)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur de filtrage par date: $e');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du filtrage d\'un passage: $e');
|
||||
return false;
|
||||
}
|
||||
}).toList();
|
||||
|
||||
// Trier par date décroissante (plus récent en premier)
|
||||
filtered.sort((a, b) {
|
||||
try {
|
||||
final DateTime dateA = a['date'] as DateTime;
|
||||
final DateTime dateB = b['date'] as DateTime;
|
||||
return dateB.compareTo(dateA);
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
debugPrint(
|
||||
'Passages filtrés: ${filtered.length}/${_formattedPassages.length}');
|
||||
return filtered;
|
||||
} catch (e) {
|
||||
debugPrint('Erreur globale lors du filtrage: $e');
|
||||
return _formattedPassages;
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour le filtre par secteur
|
||||
@@ -230,7 +365,8 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DotsPainter(),
|
||||
child: Container(width: double.infinity, height: double.infinity),
|
||||
child: const SizedBox(
|
||||
width: double.infinity, height: double.infinity),
|
||||
),
|
||||
),
|
||||
const Center(
|
||||
@@ -258,67 +394,71 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
),
|
||||
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
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Titre de la page
|
||||
Text(
|
||||
'Historique des passages',
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final passages = _getFilteredPassages();
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight - 32, // Moins le padding
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Titre de la page
|
||||
Text(
|
||||
'Historique des passages',
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Filtres supplémentaires (secteur, utilisateur, période)
|
||||
_buildAdditionalFilters(context),
|
||||
// Filtres supplémentaires (secteur, utilisateur, période)
|
||||
_buildAdditionalFilters(context),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Widget de liste des passages
|
||||
Expanded(
|
||||
child: PassagesListWidget(
|
||||
passages: _formattedPassages,
|
||||
showFilters: true,
|
||||
showSearch: true,
|
||||
showActions: true,
|
||||
initialSearchQuery: searchQuery,
|
||||
initialTypeFilter: selectedType,
|
||||
initialPaymentFilter: selectedPaymentMethod,
|
||||
// Exclure les passages de type 2 (À finaliser)
|
||||
excludePassageTypes: [2],
|
||||
// Filtres par utilisateur et secteur
|
||||
filterByUserId: selectedUserId,
|
||||
filterBySectorId: selectedSectorId,
|
||||
// Période par défaut (dernier mois)
|
||||
periodFilter: 'lastMonth',
|
||||
// Plage de dates personnalisée si définie
|
||||
dateRange: selectedDateRange,
|
||||
onPassageSelected: (passage) {
|
||||
_showDetailsDialog(context, passage);
|
||||
},
|
||||
onReceiptView: (passage) {
|
||||
_showReceiptDialog(context, passage);
|
||||
},
|
||||
onDetailsView: (passage) {
|
||||
_showDetailsDialog(context, passage);
|
||||
},
|
||||
onPassageEdit: (passage) {
|
||||
// Action pour modifier le passage
|
||||
// Cette fonctionnalité pourrait être implémentée ultérieurement
|
||||
},
|
||||
// Widget de liste des passages avec hauteur fixe
|
||||
SizedBox(
|
||||
height: constraints.maxHeight * 0.7, // 70% de la hauteur disponible
|
||||
child: PassagesListWidget(
|
||||
passages: passages,
|
||||
showFilters:
|
||||
false, // Désactivé car les filtres sont maintenant dans la card "Filtres avancés"
|
||||
showSearch:
|
||||
false, // Désactivé car la recherche est maintenant dans la card "Filtres avancés"
|
||||
showActions: true,
|
||||
// Ne plus passer les filtres individuels car ils sont maintenant appliqués dans _getFilteredPassages()
|
||||
onPassageSelected: (passage) {
|
||||
_openPassageEditDialog(context, passage);
|
||||
},
|
||||
onReceiptView: (passage) {
|
||||
_showReceiptDialog(context, passage);
|
||||
},
|
||||
onDetailsView: (passage) {
|
||||
_showDetailsDialog(context, passage);
|
||||
},
|
||||
onPassageEdit: (passage) {
|
||||
// Action pour modifier le passage
|
||||
// Cette fonctionnalité pourrait être implémentée ultérieurement
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -339,7 +479,8 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DotsPainter(),
|
||||
child: Container(width: double.infinity, height: double.infinity),
|
||||
child:
|
||||
const SizedBox(width: double.infinity, height: double.infinity),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
@@ -388,14 +529,16 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
List<Map<String, dynamic>> _formatPassagesForWidget(
|
||||
List<PassageModel> passages,
|
||||
SectorRepository sectorRepository,
|
||||
UserRepository userRepository) {
|
||||
MembreRepository membreRepository) {
|
||||
return passages.map((passage) {
|
||||
// Récupérer le secteur associé au passage
|
||||
final SectorModel? sector =
|
||||
sectorRepository.getSectorById(passage.fkSector);
|
||||
// Récupérer le secteur associé au passage (si fkSector n'est pas null)
|
||||
final SectorModel? sector = passage.fkSector != null
|
||||
? sectorRepository.getSectorById(passage.fkSector!)
|
||||
: null;
|
||||
|
||||
// Récupérer l'utilisateur associé au passage
|
||||
final UserModel? user = userRepository.getUserById(passage.fkUser);
|
||||
// Récupérer le membre associé au passage
|
||||
final MembreModel? membre =
|
||||
membreRepository.getMembreById(passage.fkUser);
|
||||
|
||||
// Construire l'adresse complète
|
||||
final String address =
|
||||
@@ -406,12 +549,21 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
|
||||
return {
|
||||
'id': passage.id,
|
||||
'date': passage.passedAt,
|
||||
'address': address,
|
||||
if (passage.passedAt != null) 'date': passage.passedAt!,
|
||||
'address': address, // Adresse complète pour l'affichage
|
||||
// Champs séparés pour l'édition
|
||||
'numero': passage.numero,
|
||||
'rueBis': passage.rueBis,
|
||||
'rue': passage.rue,
|
||||
'ville': passage.ville,
|
||||
'residence': passage.residence,
|
||||
'appt': passage.appt,
|
||||
'niveau': passage.niveau,
|
||||
'fkHabitat': passage.fkHabitat,
|
||||
'fkSector': passage.fkSector,
|
||||
'sector': sector?.libelle ?? 'Secteur inconnu',
|
||||
'fkUser': passage.fkUser,
|
||||
'user': user?.name ?? 'Utilisateur inconnu',
|
||||
'user': membre?.name ?? 'Membre inconnu',
|
||||
'type': passage.fkType,
|
||||
'amount': double.tryParse(passage.montant) ?? 0.0,
|
||||
'payment': passage.fkTypeReglement,
|
||||
@@ -421,7 +573,14 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
'notes': passage.remarque,
|
||||
'name': passage.name,
|
||||
'phone': passage.phone,
|
||||
// Ajouter d'autres champs nécessaires pour le widget
|
||||
'montant': passage.montant,
|
||||
'remarque': passage.remarque,
|
||||
// Autres champs utiles
|
||||
'fkOperation': passage.fkOperation,
|
||||
'passedAt': passage.passedAt,
|
||||
'lastSyncedAt': passage.lastSyncedAt,
|
||||
'isActive': passage.isActive,
|
||||
'isSynced': passage.isSynced,
|
||||
};
|
||||
}).toList();
|
||||
}
|
||||
@@ -552,6 +711,63 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _openPassageEditDialog(
|
||||
BuildContext context, Map<String, dynamic> passage) async {
|
||||
try {
|
||||
debugPrint('=== DEBUT _openPassageEditDialog ===');
|
||||
|
||||
// Récupérer l'ID du passage
|
||||
final int passageId = passage['id'] as int;
|
||||
debugPrint('Recherche du passage ID: $passageId');
|
||||
|
||||
// Trouver le PassageModel original dans la liste
|
||||
final PassageModel? passageModel =
|
||||
_originalPassages.where((p) => p.id == passageId).firstOrNull;
|
||||
|
||||
if (passageModel == null) {
|
||||
throw Exception('Passage original introuvable avec l\'ID: $passageId');
|
||||
}
|
||||
|
||||
debugPrint('PassageModel original trouvé');
|
||||
if (!mounted) {
|
||||
debugPrint('Widget non monté, abandon');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('Ouverture du dialog...');
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => PassageFormDialog(
|
||||
passage: passageModel,
|
||||
title: 'Modifier le passage',
|
||||
passageRepository: _passageRepository,
|
||||
userRepository: _userRepository,
|
||||
operationRepository: operationRepository,
|
||||
onSuccess: () {
|
||||
debugPrint('Dialog fermé avec succès');
|
||||
// Recharger les données après modification
|
||||
_loadPassages();
|
||||
},
|
||||
),
|
||||
);
|
||||
debugPrint('=== FIN _openPassageEditDialog ===');
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('=== ERREUR _openPassageEditDialog ===');
|
||||
debugPrint('Erreur: $e');
|
||||
debugPrint('StackTrace: $stackTrace');
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erreur lors de l\'ouverture du formulaire: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
@@ -616,25 +832,52 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Champ de recherche
|
||||
_buildSearchField(theme),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Disposition des filtres en fonction de la taille de l'écran
|
||||
isDesktop
|
||||
? Row(
|
||||
? Column(
|
||||
children: [
|
||||
// Filtre par secteur
|
||||
Expanded(
|
||||
child: _buildSectorFilter(theme, _sectors),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Première ligne : Secteur, Utilisateur, Période
|
||||
Row(
|
||||
children: [
|
||||
// Filtre par secteur
|
||||
Expanded(
|
||||
child: _buildSectorFilter(theme, _sectors),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Filtre par utilisateur
|
||||
Expanded(
|
||||
child: _buildUserFilter(theme, _users),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Filtre par membre
|
||||
Expanded(
|
||||
child: _buildMembreFilter(theme, _membres),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Filtre par période
|
||||
Expanded(
|
||||
child: _buildPeriodFilter(theme),
|
||||
// Filtre par période
|
||||
Expanded(
|
||||
child: _buildPeriodFilter(theme),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Deuxième ligne : Type de passage, Mode de règlement
|
||||
Row(
|
||||
children: [
|
||||
// Filtre par type de passage
|
||||
Expanded(
|
||||
child: _buildTypeFilter(theme),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Filtre par mode de règlement
|
||||
Expanded(
|
||||
child: _buildPaymentFilter(theme),
|
||||
),
|
||||
// Espacement pour équilibrer avec la ligne du dessus (3 colonnes)
|
||||
const Expanded(child: SizedBox()),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -644,12 +887,20 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
_buildSectorFilter(theme, _sectors),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Filtre par utilisateur
|
||||
_buildUserFilter(theme, _users),
|
||||
// Filtre par membre
|
||||
_buildMembreFilter(theme, _membres),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Filtre par période
|
||||
_buildPeriodFilter(theme),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Filtre par type de passage
|
||||
_buildTypeFilter(theme),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Filtre par mode de règlement
|
||||
_buildPaymentFilter(theme),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -714,7 +965,7 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
}),
|
||||
],
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
@@ -745,11 +996,52 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// Construction du filtre par utilisateur
|
||||
Widget _buildUserFilter(ThemeData theme, List<UserModel> users) {
|
||||
// Vérifier si la liste des utilisateurs est vide ou si selectedUser n'est pas dans la liste
|
||||
bool isSelectedUserValid = selectedUser == 'Tous' ||
|
||||
users.any((u) => (u.name ?? 'Utilisateur inconnu') == selectedUser);
|
||||
// Construction du filtre par membre
|
||||
Widget _buildMembreFilter(ThemeData theme, List<MembreModel> membres) {
|
||||
// Fonction pour formater le nom d'affichage d'un membre
|
||||
String formatMembreDisplayName(MembreModel membre) {
|
||||
final String firstName = membre.firstName ?? '';
|
||||
final String name = membre.name ?? '';
|
||||
final String sectName = membre.sectName ?? '';
|
||||
|
||||
// Construire le nom de base
|
||||
String displayName = '';
|
||||
if (firstName.isNotEmpty && name.isNotEmpty) {
|
||||
displayName = '$firstName $name';
|
||||
} else if (name.isNotEmpty) {
|
||||
displayName = name;
|
||||
} else if (firstName.isNotEmpty) {
|
||||
displayName = firstName;
|
||||
} else {
|
||||
displayName = 'Membre inconnu';
|
||||
}
|
||||
|
||||
// Ajouter le sectName entre parenthèses s'il existe
|
||||
if (sectName.isNotEmpty) {
|
||||
displayName = '$displayName ($sectName)';
|
||||
}
|
||||
|
||||
return displayName;
|
||||
}
|
||||
|
||||
// Trier les membres par nom de famille
|
||||
final List<MembreModel> sortedMembres = [...membres];
|
||||
sortedMembres.sort((a, b) {
|
||||
final String nameA = a.name ?? '';
|
||||
final String nameB = b.name ?? '';
|
||||
return nameA.compareTo(nameB);
|
||||
});
|
||||
|
||||
// Créer une map pour retrouver les membres par leur nom d'affichage
|
||||
final Map<String, MembreModel> membreDisplayMap = {};
|
||||
for (final membre in sortedMembres) {
|
||||
final displayName = formatMembreDisplayName(membre);
|
||||
membreDisplayMap[displayName] = membre;
|
||||
}
|
||||
|
||||
// Vérifier si la liste des membres est vide ou si selectedUser n'est pas dans la liste
|
||||
bool isSelectedUserValid =
|
||||
selectedUser == 'Tous' || membreDisplayMap.containsKey(selectedUser);
|
||||
|
||||
// Si selectedUser n'est pas valide, le réinitialiser à 'Tous'
|
||||
if (!isSelectedUserValid) {
|
||||
@@ -767,7 +1059,7 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Utilisateur',
|
||||
'Membre',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -788,19 +1080,18 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
items: [
|
||||
const DropdownMenuItem<String>(
|
||||
value: 'Tous',
|
||||
child: Text('Tous les utilisateurs'),
|
||||
child: Text('Tous les membres'),
|
||||
),
|
||||
...users.map((user) {
|
||||
// S'assurer que user.name n'est pas null
|
||||
final String userName = user.name ?? 'Utilisateur inconnu';
|
||||
...membreDisplayMap.entries.map((entry) {
|
||||
final String displayName = entry.key;
|
||||
return DropdownMenuItem<String>(
|
||||
value: userName,
|
||||
value: displayName,
|
||||
child: Text(
|
||||
userName,
|
||||
displayName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
}),
|
||||
],
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
@@ -808,21 +1099,16 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
_updateUserFilter('Tous', null);
|
||||
} else {
|
||||
try {
|
||||
// Trouver l'utilisateur correspondant
|
||||
final user = users.firstWhere(
|
||||
(u) => (u.name ?? 'Utilisateur inconnu') == value,
|
||||
orElse: () => users.isNotEmpty
|
||||
? users.first
|
||||
: throw Exception('Liste d\'utilisateurs vide'),
|
||||
);
|
||||
// S'assurer que user.name et user.id ne sont pas null
|
||||
final String userName =
|
||||
user.name ?? 'Utilisateur inconnu';
|
||||
final int? userId = user.id;
|
||||
_updateUserFilter(userName, userId);
|
||||
// Trouver le membre correspondant dans la map
|
||||
final membre = membreDisplayMap[value];
|
||||
if (membre != null) {
|
||||
final int membreId = membre.id;
|
||||
_updateUserFilter(value, membreId);
|
||||
} else {
|
||||
throw Exception('Membre non trouvé: $value');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'Erreur lors de la sélection de l\'utilisateur: $e');
|
||||
debugPrint('Erreur lors de la sélection du membre: $e');
|
||||
_updateUserFilter('Tous', null);
|
||||
}
|
||||
}
|
||||
@@ -912,34 +1198,155 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showResendConfirmation(BuildContext context, int passageId) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Renvoyer le reçu'),
|
||||
content: Text(
|
||||
'Êtes-vous sûr de vouloir renvoyer le reçu du passage #$passageId ?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
// Construction du champ de recherche
|
||||
Widget _buildSearchField(ThemeData theme) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Recherche',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Action pour renvoyer le reçu
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content:
|
||||
Text('Reçu du passage #$passageId renvoyé avec succès'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Rechercher par adresse ou nom...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_searchController.clear();
|
||||
searchQuery = '';
|
||||
});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
searchQuery = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Construction du filtre par type de passage
|
||||
Widget _buildTypeFilter(ThemeData theme) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Type de passage',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.colorScheme.outline),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: selectedType,
|
||||
isExpanded: true,
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
items: [
|
||||
const DropdownMenuItem<String>(
|
||||
value: 'Tous',
|
||||
child: Text('Tous les types'),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Renvoyer'),
|
||||
...AppKeys.typesPassages.entries.map((entry) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: entry.key.toString(),
|
||||
child: Text(
|
||||
entry.value['titre'] as String,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
selectedType = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Construction du filtre par mode de règlement
|
||||
Widget _buildPaymentFilter(ThemeData theme) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Mode de règlement',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: theme.colorScheme.outline),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: selectedPaymentMethod,
|
||||
isExpanded: true,
|
||||
icon: const Icon(Icons.arrow_drop_down),
|
||||
items: [
|
||||
const DropdownMenuItem<String>(
|
||||
value: 'Tous',
|
||||
child: Text('Tous les modes'),
|
||||
),
|
||||
...AppKeys.typesReglements.entries.map((entry) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: entry.key.toString(),
|
||||
child: Text(
|
||||
entry.value['titre'] as String,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
onChanged: (String? value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
selectedPaymentMethod = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
3585
app/lib/presentation/admin/admin_map_page.dart
Normal file → Executable file
3585
app/lib/presentation/admin/admin_map_page.dart
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
0
app/lib/presentation/admin/admin_operations_page.dart
Normal file → Executable file
0
app/lib/presentation/admin/admin_operations_page.dart
Normal file → Executable file
17
app/lib/presentation/admin/admin_statistics_page.dart
Normal file → Executable file
17
app/lib/presentation/admin/admin_statistics_page.dart
Normal file → Executable file
@@ -27,7 +27,7 @@ class DotsPainter extends CustomPainter {
|
||||
}
|
||||
|
||||
class AdminStatisticsPage extends StatefulWidget {
|
||||
const AdminStatisticsPage({Key? key}) : super(key: key);
|
||||
const AdminStatisticsPage({super.key});
|
||||
|
||||
@override
|
||||
State<AdminStatisticsPage> createState() => _AdminStatisticsPageState();
|
||||
@@ -80,7 +80,8 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
|
||||
),
|
||||
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
|
||||
@@ -228,21 +229,21 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
|
||||
PaymentData(
|
||||
typeId: 1,
|
||||
amount: 1500.0,
|
||||
color: const Color(0xFFFFC107),
|
||||
color: Color(0xFFFFC107),
|
||||
icon: Icons.toll,
|
||||
title: 'Espèce',
|
||||
),
|
||||
PaymentData(
|
||||
typeId: 2,
|
||||
amount: 2500.0,
|
||||
color: const Color(0xFF8BC34A),
|
||||
color: Color(0xFF8BC34A),
|
||||
icon: Icons.wallet,
|
||||
title: 'Chèque',
|
||||
),
|
||||
PaymentData(
|
||||
typeId: 3,
|
||||
amount: 1000.0,
|
||||
color: const Color(0xFF00B0FF),
|
||||
color: Color(0xFF00B0FF),
|
||||
icon: Icons.credit_card,
|
||||
title: 'CB',
|
||||
),
|
||||
@@ -281,21 +282,21 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
|
||||
PaymentData(
|
||||
typeId: 1,
|
||||
amount: 1500.0,
|
||||
color: const Color(0xFFFFC107),
|
||||
color: Color(0xFFFFC107),
|
||||
icon: Icons.toll,
|
||||
title: 'Espèce',
|
||||
),
|
||||
PaymentData(
|
||||
typeId: 2,
|
||||
amount: 2500.0,
|
||||
color: const Color(0xFF8BC34A),
|
||||
color: Color(0xFF8BC34A),
|
||||
icon: Icons.wallet,
|
||||
title: 'Chèque',
|
||||
),
|
||||
PaymentData(
|
||||
typeId: 3,
|
||||
amount: 1000.0,
|
||||
color: const Color(0xFF00B0FF),
|
||||
color: Color(0xFF00B0FF),
|
||||
icon: Icons.credit_card,
|
||||
title: 'CB',
|
||||
),
|
||||
|
||||
504
app/lib/presentation/auth/login_page.dart
Normal file → Executable file
504
app/lib/presentation/auth/login_page.dart
Normal file → Executable file
@@ -2,14 +2,13 @@ import 'package:flutter/material.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb, kDebugMode;
|
||||
import 'dart:js' as js;
|
||||
import 'package:geosector_app/core/services/js_stub.dart'
|
||||
if (dart.library.js) 'dart:js' as js;
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:geosector_app/core/services/app_info_service.dart';
|
||||
import 'package:geosector_app/presentation/widgets/custom_button.dart';
|
||||
import 'package:geosector_app/presentation/widgets/custom_text_field.dart';
|
||||
import 'package:geosector_app/core/services/location_service.dart';
|
||||
import 'package:geosector_app/core/services/connectivity_service.dart';
|
||||
import 'package:geosector_app/presentation/widgets/connectivity_indicator.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
||||
@@ -57,10 +56,6 @@ class _LoginPageState extends State<LoginPage> {
|
||||
// Type de connexion (utilisateur ou administrateur)
|
||||
late String _loginType;
|
||||
|
||||
// État des permissions de géolocalisation
|
||||
bool _checkingPermission = true;
|
||||
bool _hasLocationPermission = false;
|
||||
String? _locationErrorMessage;
|
||||
|
||||
// État de la connexion Internet
|
||||
bool _isConnected = false;
|
||||
@@ -78,7 +73,9 @@ 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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -101,7 +98,8 @@ 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('/');
|
||||
});
|
||||
@@ -154,10 +152,13 @@ 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) {
|
||||
@@ -170,16 +171,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier les permissions de géolocalisation au démarrage seulement sur mobile
|
||||
if (!kIsWeb) {
|
||||
_checkLocationPermission();
|
||||
} else {
|
||||
// En version web, on considère que les permissions sont accordées
|
||||
setState(() {
|
||||
_checkingPermission = false;
|
||||
_hasLocationPermission = true;
|
||||
});
|
||||
}
|
||||
// Les permissions sont maintenant vérifiées dans splash_page
|
||||
|
||||
// Initialiser l'état de la connexion
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -221,7 +213,8 @@ 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
|
||||
@@ -235,40 +228,17 @@ 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');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Vérifie les permissions de géolocalisation
|
||||
Future<void> _checkLocationPermission() async {
|
||||
// Ne pas vérifier les permissions en version web
|
||||
if (kIsWeb) {
|
||||
setState(() {
|
||||
_hasLocationPermission = true;
|
||||
_checkingPermission = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_checkingPermission = true;
|
||||
});
|
||||
|
||||
// Vérifier si les services de localisation sont activés et si l'application a la permission
|
||||
final hasPermission = await LocationService.checkAndRequestPermission();
|
||||
final errorMessage = await LocationService.getLocationErrorMessage();
|
||||
|
||||
setState(() {
|
||||
_hasLocationPermission = hasPermission;
|
||||
_locationErrorMessage = errorMessage;
|
||||
_checkingPermission = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -278,210 +248,6 @@ class _LoginPageState extends State<LoginPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Construit l'écran de chargement pendant la vérification des permissions
|
||||
Widget _buildLoadingScreen(ThemeData theme) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Logo simlifié
|
||||
Image.asset(
|
||||
'assets/images/logo-geosector-1024.png',
|
||||
height: 160,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Vérification des permissions...',
|
||||
style: theme.textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit l'écran de demande de permission de géolocalisation
|
||||
Widget _buildLocationPermissionScreen(ThemeData theme) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
// Fond dégradé avec petits points blancs
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: _loginType == 'user' ? [Colors.white, Colors.green.shade300] : [Colors.white, Colors.red.shade300],
|
||||
),
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DotsPainter(),
|
||||
child: const SizedBox(width: double.infinity, height: double.infinity),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: ConstrainedBox(
|
||||
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)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Logo simplifié
|
||||
Image.asset(
|
||||
'assets/images/logo-geosector-1024.png',
|
||||
height: 160,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Accès à la localisation requis',
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Message d'erreur
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: theme.colorScheme.error.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_disabled,
|
||||
color: theme.colorScheme.error,
|
||||
size: 48,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_locationErrorMessage ?? 'L\'accès à la localisation est nécessaire pour utiliser cette application.',
|
||||
style: theme.textTheme.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Cette application utilise votre position pour enregistrer les passages et assurer le suivi des secteurs géographiques. Sans cette permission, l\'application ne peut pas fonctionner correctement.',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Instructions pour activer la localisation
|
||||
Text(
|
||||
'Comment activer la localisation :',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
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'),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Boutons d'action
|
||||
CustomButton(
|
||||
onPressed: () async {
|
||||
// Ouvrir les paramètres de l'application
|
||||
await LocationService.openAppSettings();
|
||||
},
|
||||
text: 'Ouvrir les paramètres de l\'application',
|
||||
icon: Icons.settings,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
CustomButton(
|
||||
onPressed: () async {
|
||||
// Ouvrir les paramètres de localisation
|
||||
await LocationService.openLocationSettings();
|
||||
},
|
||||
text: 'Ouvrir les paramètres de localisation',
|
||||
icon: Icons.location_on,
|
||||
backgroundColor: theme.colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
CustomButton(
|
||||
onPressed: () {
|
||||
// Vérifier à nouveau les permissions
|
||||
_checkLocationPermission();
|
||||
},
|
||||
text: 'Vérifier à nouveau',
|
||||
icon: Icons.refresh,
|
||||
backgroundColor: theme.colorScheme.tertiary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit une étape d'instruction pour activer la localisation
|
||||
Widget _buildInstructionStep(ThemeData theme, int stepNumber, String instruction) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'$stepNumber',
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
instruction,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -491,12 +257,8 @@ class _LoginPageState extends State<LoginPage> {
|
||||
final theme = Theme.of(context);
|
||||
final size = MediaQuery.of(context).size;
|
||||
|
||||
// Afficher l'écran de permission de géolocalisation si l'utilisateur n'a pas accordé la permission (sauf en version web)
|
||||
if (!kIsWeb && _checkingPermission) {
|
||||
return _buildLoadingScreen(theme);
|
||||
} else if (!kIsWeb && !_hasLocationPermission) {
|
||||
return _buildLocationPermissionScreen(theme);
|
||||
}
|
||||
// Les permissions sont maintenant gérées dans splash_page
|
||||
// On n'a plus besoin de ces vérifications ici
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
@@ -508,12 +270,15 @@ 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: const SizedBox(width: double.infinity, height: double.infinity),
|
||||
child: const SizedBox(
|
||||
width: double.infinity, height: double.infinity),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
@@ -524,8 +289,11 @@ 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(
|
||||
@@ -539,10 +307,14 @@ 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,
|
||||
),
|
||||
@@ -551,14 +323,16 @@ class _LoginPageState extends State<LoginPage> {
|
||||
if (kDebugMode)
|
||||
Text(
|
||||
'Type de connexion: $_loginType',
|
||||
style: const 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.onSurface.withOpacity(0.7),
|
||||
color:
|
||||
theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -576,17 +350,23 @@ 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.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -623,7 +403,9 @@ 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(() {
|
||||
@@ -638,17 +420,21 @@ 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,
|
||||
@@ -656,12 +442,16 @@ 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,
|
||||
),
|
||||
);
|
||||
@@ -671,25 +461,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;
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
@@ -718,44 +515,45 @@ class _LoginPageState extends State<LoginPage> {
|
||||
|
||||
// Bouton de connexion
|
||||
CustomButton(
|
||||
onPressed: (userRepository.isLoading || !_isConnected)
|
||||
onPressed: (userRepository.isLoading ||
|
||||
!_isConnected)
|
||||
? null
|
||||
: () async {
|
||||
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(
|
||||
const SnackBar(
|
||||
content: Text('L\'accès à la localisation est nécessaire pour utiliser cette application.'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_formKey.currentState!
|
||||
.validate()) {
|
||||
// Les permissions sont déjà vérifiées dans splash_page
|
||||
|
||||
// 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -768,15 +566,18 @@ 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,
|
||||
@@ -784,15 +585,20 @@ 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,
|
||||
),
|
||||
);
|
||||
@@ -802,32 +608,41 @@ 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;
|
||||
}
|
||||
|
||||
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),
|
||||
@@ -950,7 +765,8 @@ 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;
|
||||
@@ -1007,8 +823,10 @@ 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),
|
||||
@@ -1018,8 +836,10 @@ 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) {
|
||||
@@ -1027,7 +847,8 @@ 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');
|
||||
}
|
||||
|
||||
@@ -1044,7 +865,8 @@ class _LoginPageState extends State<LoginPage> {
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
// Fermer automatiquement la boîte de dialogue après 2 secondes
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
Future.delayed(const Duration(seconds: 2),
|
||||
() {
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
@@ -1076,13 +898,16 @@ 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,
|
||||
@@ -1107,7 +932,8 @@ class _LoginPageState extends State<LoginPage> {
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: const Text('Recevoir un nouveau mot de passe'),
|
||||
|
||||
287
app/lib/presentation/auth/register_page.dart
Normal file → Executable file
287
app/lib/presentation/auth/register_page.dart
Normal file → Executable file
@@ -6,10 +6,8 @@ import 'dart:math' as math;
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:geosector_app/core/repositories/user_repository.dart';
|
||||
import 'package:geosector_app/presentation/widgets/custom_button.dart';
|
||||
import 'package:geosector_app/presentation/widgets/custom_text_field.dart';
|
||||
import 'package:geosector_app/core/services/connectivity_service.dart';
|
||||
import 'package:geosector_app/presentation/widgets/connectivity_indicator.dart';
|
||||
import 'package:geosector_app/core/services/app_info_service.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
@@ -73,8 +71,10 @@ 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;
|
||||
@@ -100,7 +100,9 @@ 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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -164,7 +166,8 @@ 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(
|
||||
@@ -246,7 +249,8 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
),
|
||||
child: CustomPaint(
|
||||
painter: DotsPainter(),
|
||||
child: const SizedBox(width: double.infinity, height: double.infinity),
|
||||
child: const SizedBox(
|
||||
width: double.infinity, height: double.infinity),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
@@ -289,7 +293,8 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
if (mounted && _isConnected != isConnected) {
|
||||
setState(() {
|
||||
_isConnected = isConnected;
|
||||
_connectionType = connectivityService.connectionType;
|
||||
_connectionType =
|
||||
connectivityService.connectionType;
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -336,7 +341,8 @@ 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,
|
||||
),
|
||||
);
|
||||
@@ -388,7 +394,8 @@ 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;
|
||||
@@ -415,7 +422,8 @@ 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,
|
||||
@@ -443,7 +451,8 @@ 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.onSurface,
|
||||
),
|
||||
@@ -473,7 +482,8 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
),
|
||||
child: _isLoadingCities
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 16),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
@@ -485,16 +495,20 @@ 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,
|
||||
),
|
||||
@@ -512,13 +526,18 @@ 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);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -553,7 +572,8 @@ 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,
|
||||
@@ -590,30 +610,43 @@ 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -625,11 +658,15 @@ 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,
|
||||
),
|
||||
);
|
||||
@@ -640,11 +677,16 @@ 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,
|
||||
};
|
||||
|
||||
@@ -656,12 +698,14 @@ 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),
|
||||
);
|
||||
@@ -672,23 +716,34 @@ 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: [
|
||||
@@ -697,50 +752,88 @@ 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,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(
|
||||
height: 16),
|
||||
Text(
|
||||
'Vous allez recevoir un email contenant :',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
style: theme
|
||||
.textTheme
|
||||
.bodyMedium,
|
||||
),
|
||||
const 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),
|
||||
Icon(
|
||||
Icons
|
||||
.arrow_right,
|
||||
size: 20,
|
||||
color: theme
|
||||
.colorScheme
|
||||
.primary),
|
||||
const SizedBox(
|
||||
width: 4),
|
||||
const Expanded(
|
||||
child: Text('Votre identifiant de connexion'),
|
||||
child: Text(
|
||||
'Votre identifiant de connexion'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const 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),
|
||||
const SizedBox(width: 4),
|
||||
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'),
|
||||
child: Text(
|
||||
'Un lien pour définir votre mot de passe'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -748,15 +841,27 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
// Rediriger vers la page de connexion
|
||||
context.go('/login');
|
||||
Navigator.of(
|
||||
context)
|
||||
.pop();
|
||||
// Rediriger vers splash avec redirection automatique vers login admin
|
||||
context
|
||||
.go('/?action=login&type=admin');
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.primary,
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||
style: TextButton
|
||||
.styleFrom(
|
||||
foregroundColor:
|
||||
theme
|
||||
.colorScheme
|
||||
.primary,
|
||||
textStyle:
|
||||
const TextStyle(
|
||||
fontWeight:
|
||||
FontWeight
|
||||
.bold),
|
||||
),
|
||||
child: const Text('OK'),
|
||||
child:
|
||||
const Text('OK'),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -769,16 +874,21 @@ 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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -786,7 +896,8 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
);
|
||||
|
||||
// Afficher également un SnackBar
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Colors.red,
|
||||
@@ -797,9 +908,11 @@ 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,
|
||||
),
|
||||
);
|
||||
@@ -813,9 +926,11 @@ 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,
|
||||
),
|
||||
);
|
||||
@@ -823,7 +938,9 @@ 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),
|
||||
@@ -838,7 +955,7 @@ class _RegisterPageState extends State<RegisterPage> {
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.go('/login');
|
||||
context.go('/?action=login&type=admin');
|
||||
},
|
||||
child: Text(
|
||||
'Se connecter',
|
||||
|
||||
309
app/lib/presentation/auth/splash_page.dart
Normal file → Executable file
309
app/lib/presentation/auth/splash_page.dart
Normal file → Executable file
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:geosector_app/core/services/app_info_service.dart';
|
||||
import 'package:geosector_app/core/services/hive_service.dart';
|
||||
import 'package:geosector_app/core/services/location_service.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
@@ -9,7 +10,13 @@ import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SplashPage extends StatefulWidget {
|
||||
const SplashPage({super.key});
|
||||
/// Action à effectuer après l'initialisation (login ou register)
|
||||
final String? action;
|
||||
|
||||
/// Type de login/register (user ou admin) - ignoré pour register
|
||||
final String? type;
|
||||
|
||||
const SplashPage({super.key, this.action, this.type});
|
||||
|
||||
@override
|
||||
State<SplashPage> createState() => _SplashPageState();
|
||||
@@ -46,6 +53,8 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
double _progress = 0.0;
|
||||
bool _showButtons = false;
|
||||
String _appVersion = '';
|
||||
bool _showLocationError = false;
|
||||
String? _locationErrorMessage;
|
||||
|
||||
Future<void> _getAppVersion() async {
|
||||
try {
|
||||
@@ -100,49 +109,127 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
try {
|
||||
debugPrint('🚀 Début de l\'initialisation complète de l\'application...');
|
||||
|
||||
// Étape 1: Initialisation complète de Hive avec HiveService
|
||||
// Étape 0: Vérification des permissions GPS (obligatoire) - 0 à 10%
|
||||
if (!kIsWeb) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Vérification des autorisations GPS...";
|
||||
_progress = 0.05;
|
||||
});
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
final hasPermission = await LocationService.checkAndRequestPermission();
|
||||
final errorMessage = await LocationService.getLocationErrorMessage();
|
||||
|
||||
if (!hasPermission) {
|
||||
// Si les permissions ne sont pas accordées, on arrête tout
|
||||
debugPrint('❌ Permissions GPS refusées');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_showLocationError = true;
|
||||
_locationErrorMessage = errorMessage ?? "L'application nécessite l'accès à votre position pour fonctionner correctement.";
|
||||
_isInitializing = false;
|
||||
_progress = 0.0;
|
||||
});
|
||||
}
|
||||
return; // On arrête l'initialisation ici
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Autorisations GPS accordées...";
|
||||
_progress = 0.10;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Étape 1: Préparation - 10 à 15%
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Initialisation de la base de données...";
|
||||
_progress = 0.1;
|
||||
_statusMessage = "Démarrage de l'application...";
|
||||
_progress = 0.12;
|
||||
});
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 200)); // Petit délai pour voir le début
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Chargement des composants...";
|
||||
_progress = 0.15;
|
||||
});
|
||||
}
|
||||
|
||||
// HiveService fait TOUT le travail lourd (adaptateurs, destruction, recréation)
|
||||
// Étape 2: Initialisation Hive - 15 à 60% (étape la plus longue)
|
||||
await HiveService.instance.initializeAndResetHive();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Vérification des bases de données...";
|
||||
_progress = 0.7;
|
||||
_statusMessage = "Configuration du stockage...";
|
||||
_progress = 0.45;
|
||||
});
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 300)); // Simulation du temps de traitement
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Préparation des données...";
|
||||
_progress = 0.60;
|
||||
});
|
||||
}
|
||||
|
||||
// Étape 2: S'assurer que toutes les Box sont ouvertes
|
||||
// Étape 3: Ouverture des Box - 60 à 80%
|
||||
await HiveService.instance.ensureBoxesAreOpen();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Finalisation...";
|
||||
_progress = 0.9;
|
||||
_statusMessage = "Vérification du système...";
|
||||
_progress = 0.80;
|
||||
});
|
||||
}
|
||||
|
||||
// Étape 3: Vérification finale
|
||||
// Étape 4: Vérification finale - 80 à 95%
|
||||
final allBoxesOpen = HiveService.instance.areAllBoxesOpen();
|
||||
if (!allBoxesOpen) {
|
||||
final diagnostic = HiveService.instance.getDiagnostic();
|
||||
debugPrint('❌ Diagnostic des Box: $diagnostic');
|
||||
throw Exception('Certaines bases de données ne sont pas accessibles');
|
||||
throw Exception('Une erreur est survenue lors de l\'initialisation');
|
||||
}
|
||||
|
||||
// Finalisation
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Finalisation du chargement...";
|
||||
_progress = 0.95;
|
||||
});
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 300)); // Petit délai pour finaliser
|
||||
|
||||
// Étape 5: Finalisation - 95 à 100%
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Application prête !";
|
||||
_progress = 1.0;
|
||||
_isInitializing = false;
|
||||
_showButtons = true;
|
||||
});
|
||||
|
||||
// Attendre un court instant pour que l'utilisateur voie "Application prête !"
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
|
||||
setState(() {
|
||||
_isInitializing = false;
|
||||
});
|
||||
|
||||
// Redirection automatique si des paramètres sont fournis
|
||||
if (widget.action != null) {
|
||||
await _handleAutoRedirect();
|
||||
} else {
|
||||
setState(() {
|
||||
_showButtons = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ Initialisation complète de l\'application terminée avec succès');
|
||||
@@ -151,7 +238,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = "Erreur d'initialisation - Redémarrage recommandé";
|
||||
_statusMessage = "Erreur de chargement - Veuillez redémarrer l'application";
|
||||
_progress = 1.0;
|
||||
_isInitializing = false;
|
||||
_showButtons = true;
|
||||
@@ -160,6 +247,51 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère la redirection automatique après l'initialisation
|
||||
Future<void> _handleAutoRedirect() async {
|
||||
// Petit délai pour voir le message "Application prête !"
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
final action = widget.action?.toLowerCase();
|
||||
final type = widget.type?.toLowerCase();
|
||||
|
||||
debugPrint('🔄 Redirection automatique: action=$action, type=$type');
|
||||
|
||||
// Afficher un message de redirection avant de naviguer
|
||||
setState(() {
|
||||
_statusMessage = action == 'login'
|
||||
? "Redirection vers la connexion..."
|
||||
: action == 'register'
|
||||
? "Redirection vers l'inscription..."
|
||||
: "Redirection...";
|
||||
});
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
switch (action) {
|
||||
case 'login':
|
||||
if (type == 'admin') {
|
||||
context.go('/login/admin');
|
||||
} else {
|
||||
// Par défaut, rediriger vers user si type non spécifié ou invalid
|
||||
context.go('/login/user');
|
||||
}
|
||||
break;
|
||||
case 'register':
|
||||
// Pour register, le type n'est pas pris en compte
|
||||
context.go('/register');
|
||||
break;
|
||||
default:
|
||||
// Si action non reconnue, afficher les boutons normalement
|
||||
setState(() {
|
||||
_showButtons = true;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -243,26 +375,143 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
|
||||
const Spacer(flex: 1),
|
||||
|
||||
// Indicateur de chargement
|
||||
if (_isInitializing) ...[
|
||||
if (_isInitializing && !_showLocationError) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: LinearProgressIndicator(
|
||||
value: _progress,
|
||||
backgroundColor: Colors.grey.withOpacity(0.2),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
theme.colorScheme.primary,
|
||||
child: Column(
|
||||
children: [
|
||||
// Barre de progression avec animation
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.colorScheme.primary.withOpacity(0.2),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
curve: Curves.easeInOut,
|
||||
tween: Tween(begin: 0.0, end: _progress),
|
||||
builder: (context, value, child) {
|
||||
return LinearProgressIndicator(
|
||||
value: value,
|
||||
backgroundColor: Colors.grey.withOpacity(0.15),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
theme.colorScheme.primary,
|
||||
),
|
||||
minHeight: 12,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
minHeight: 10,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Pourcentage
|
||||
Text(
|
||||
'${(_progress * 100).round()}%',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_statusMessage,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
// Message de statut avec animation
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Text(
|
||||
_statusMessage,
|
||||
key: ValueKey(_statusMessage),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Erreur de localisation
|
||||
if (_showLocationError) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_off,
|
||||
size: 48,
|
||||
color: Colors.red.shade700,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Autorisations GPS requises',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: Colors.red.shade700,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_locationErrorMessage ?? "L'application nécessite l'accès à votre position pour fonctionner correctement.",
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.red.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Bouton Réessayer
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showLocationError = false;
|
||||
_isInitializing = true;
|
||||
});
|
||||
_startInitialization();
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Réessayer'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Bouton Paramètres
|
||||
OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
if (_locationErrorMessage?.contains('définitivement') ?? false) {
|
||||
await LocationService.openAppSettings();
|
||||
} else {
|
||||
await LocationService.openLocationSettings();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.settings),
|
||||
label: const Text('Paramètres'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.red.shade700,
|
||||
side: BorderSide(color: Colors.red.shade700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
348
app/lib/presentation/dialogs/sector_action_result_dialog.dart
Normal file
348
app/lib/presentation/dialogs/sector_action_result_dialog.dart
Normal file
@@ -0,0 +1,348 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum SectorActionType {
|
||||
create,
|
||||
update,
|
||||
delete,
|
||||
}
|
||||
|
||||
class SectorActionResultDialog extends StatelessWidget {
|
||||
final SectorActionType actionType;
|
||||
final String sectorName;
|
||||
final Map<String, dynamic> statistics;
|
||||
final Map<String, dynamic>? departmentWarning;
|
||||
final VoidCallback? onConfirm;
|
||||
|
||||
const SectorActionResultDialog({
|
||||
super.key,
|
||||
required this.actionType,
|
||||
required this.sectorName,
|
||||
required this.statistics,
|
||||
this.departmentWarning,
|
||||
this.onConfirm,
|
||||
});
|
||||
|
||||
String get _actionTitle {
|
||||
switch (actionType) {
|
||||
case SectorActionType.create:
|
||||
return 'Secteur créé';
|
||||
case SectorActionType.update:
|
||||
return 'Secteur modifié';
|
||||
case SectorActionType.delete:
|
||||
return 'Secteur supprimé';
|
||||
}
|
||||
}
|
||||
|
||||
IconData get _actionIcon {
|
||||
switch (actionType) {
|
||||
case SectorActionType.create:
|
||||
return Icons.add_location_alt;
|
||||
case SectorActionType.update:
|
||||
return Icons.edit_location_alt;
|
||||
case SectorActionType.delete:
|
||||
return Icons.delete_forever;
|
||||
}
|
||||
}
|
||||
|
||||
Color get _actionColor {
|
||||
switch (actionType) {
|
||||
case SectorActionType.create:
|
||||
return Colors.green;
|
||||
case SectorActionType.update:
|
||||
return Colors.orange;
|
||||
case SectorActionType.delete:
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(_actionIcon, color: _actionColor, size: 28),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_actionTitle,
|
||||
style: const TextStyle(fontSize: 20),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
sectorName,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Warning départemental si présent
|
||||
if (departmentWarning != null && departmentWarning!['intersecting_departments'] != null) ...[
|
||||
_buildDepartmentWarning(),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Statistiques des passages
|
||||
_buildStatisticsSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
onConfirm?.call();
|
||||
},
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDepartmentWarning() {
|
||||
final departments = departmentWarning!['intersecting_departments'] as List<dynamic>;
|
||||
final isMultipleDepartments = departments.length > 1;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.orange.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, color: Colors.orange[700], size: 24),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
isMultipleDepartments
|
||||
? 'Secteur à cheval sur plusieurs départements'
|
||||
: 'Secteur sur un autre département',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange[900],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...departments.map((dept) {
|
||||
final percentage = dept['percentage_overlap'] as num;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 32, top: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.location_on, size: 16, color: Colors.orange[700]),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${dept['nom_dept']} (${dept['code_dept']})',
|
||||
style: TextStyle(color: Colors.orange[900]),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${percentage.toStringAsFixed(1)}%',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange[900],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatisticsSection() {
|
||||
final List<Widget> statisticWidgets = [];
|
||||
|
||||
// Titre de la section
|
||||
statisticWidgets.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Text(
|
||||
'Statistiques des passages',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[800],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// CREATE : passages créés et intégrés
|
||||
if (actionType == SectorActionType.create) {
|
||||
final passagesCreated = statistics['passages_created'] ?? 0;
|
||||
final passagesIntegrated = statistics['passages_integrated'] ?? 0;
|
||||
final totalPassages = passagesCreated + passagesIntegrated;
|
||||
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.add_circle_outline,
|
||||
label: 'Nouveaux passages créés',
|
||||
value: passagesCreated,
|
||||
color: Colors.green,
|
||||
));
|
||||
|
||||
if (passagesIntegrated > 0) {
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.merge_type,
|
||||
label: 'Passages orphelins intégrés',
|
||||
value: passagesIntegrated,
|
||||
color: Colors.blue,
|
||||
));
|
||||
}
|
||||
|
||||
statisticWidgets.add(const Divider(height: 24));
|
||||
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.functions,
|
||||
label: 'Total des passages du secteur',
|
||||
value: totalPassages,
|
||||
color: Colors.indigo,
|
||||
isBold: true,
|
||||
));
|
||||
}
|
||||
|
||||
// UPDATE : passages créés, mis à jour, orphelins, total
|
||||
else if (actionType == SectorActionType.update) {
|
||||
final passagesCreated = statistics['passages_created'] ?? 0;
|
||||
final passagesUpdated = statistics['passages_updated'] ?? 0;
|
||||
final passagesOrphaned = statistics['passages_orphaned'] ?? 0;
|
||||
final passagesTotal = statistics['passages_total'] ?? 0;
|
||||
|
||||
if (passagesCreated > 0) {
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.add_circle_outline,
|
||||
label: 'Nouveaux passages créés',
|
||||
value: passagesCreated,
|
||||
color: Colors.green,
|
||||
));
|
||||
}
|
||||
|
||||
if (passagesUpdated > 0) {
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.update,
|
||||
label: 'Passages mis à jour',
|
||||
value: passagesUpdated,
|
||||
color: Colors.blue,
|
||||
));
|
||||
}
|
||||
|
||||
if (passagesOrphaned > 0) {
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.remove_circle_outline,
|
||||
label: 'Passages mis en orphelin',
|
||||
value: passagesOrphaned,
|
||||
color: Colors.orange,
|
||||
));
|
||||
}
|
||||
|
||||
statisticWidgets.add(const Divider(height: 24));
|
||||
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.functions,
|
||||
label: 'Total des passages du secteur',
|
||||
value: passagesTotal,
|
||||
color: Colors.indigo,
|
||||
isBold: true,
|
||||
));
|
||||
}
|
||||
|
||||
// DELETE : passages supprimés et conservés
|
||||
else if (actionType == SectorActionType.delete) {
|
||||
final passagesDeleted = statistics['passages_deleted'] ?? 0;
|
||||
final passagesReassigned = statistics['passages_reassigned'] ?? 0;
|
||||
|
||||
if (passagesDeleted > 0) {
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.delete_outline,
|
||||
label: 'Passages supprimés',
|
||||
value: passagesDeleted,
|
||||
color: Colors.red,
|
||||
));
|
||||
}
|
||||
|
||||
if (passagesReassigned > 0) {
|
||||
statisticWidgets.add(_buildStatRow(
|
||||
icon: Icons.bookmark_border,
|
||||
label: 'Passages conservés (orphelins)',
|
||||
value: passagesReassigned,
|
||||
color: Colors.orange,
|
||||
));
|
||||
}
|
||||
|
||||
if (passagesDeleted == 0 && passagesReassigned == 0) {
|
||||
statisticWidgets.add(
|
||||
Text(
|
||||
'Aucun passage dans ce secteur',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: statisticWidgets,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatRow({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required int value,
|
||||
required Color color,
|
||||
bool isBold = false,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value.toString(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
fontSize: isBold ? 18 : 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
423
app/lib/presentation/dialogs/sector_dialog.dart
Normal file
423
app/lib/presentation/dialogs/sector_dialog.dart
Normal file
@@ -0,0 +1,423 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/core/data/models/sector_model.dart';
|
||||
import 'package:geosector_app/core/data/models/membre_model.dart';
|
||||
import 'package:geosector_app/core/data/models/user_sector_model.dart';
|
||||
import 'package:geosector_app/core/repositories/membre_repository.dart';
|
||||
import 'package:geosector_app/core/services/current_amicale_service.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
|
||||
class SectorDialog extends StatefulWidget {
|
||||
final SectorModel? existingSector;
|
||||
final List<List<double>> coordinates;
|
||||
final Future<void> Function(String name, String color, List<int> memberIds) onSave;
|
||||
|
||||
const SectorDialog({
|
||||
super.key,
|
||||
this.existingSector,
|
||||
required this.coordinates,
|
||||
required this.onSave,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SectorDialog> createState() => _SectorDialogState();
|
||||
}
|
||||
|
||||
class _SectorDialogState extends State<SectorDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameController = TextEditingController();
|
||||
final _nameFocusNode = FocusNode();
|
||||
Color _selectedColor = Colors.blue;
|
||||
final List<int> _selectedMemberIds = [];
|
||||
bool _isLoading = false;
|
||||
bool _membersLoaded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.existingSector != null) {
|
||||
_nameController.text = widget.existingSector!.libelle;
|
||||
_selectedColor = _hexToColor(widget.existingSector!.color);
|
||||
// Charger les membres affectés au secteur
|
||||
_loadSectorMembers();
|
||||
}
|
||||
|
||||
// Donner le focus au champ nom après que le dialog soit construit
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_nameFocusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
// Charger les membres actuellement affectés au secteur
|
||||
void _loadSectorMembers() {
|
||||
if (widget.existingSector == null) return;
|
||||
|
||||
debugPrint('=== Début chargement membres pour secteur ${widget.existingSector!.id} - ${widget.existingSector!.libelle} ===');
|
||||
|
||||
try {
|
||||
// Vérifier si la box UserSector est ouverte
|
||||
if (!Hive.isBoxOpen(AppKeys.userSectorBoxName)) {
|
||||
debugPrint('Box UserSector non ouverte');
|
||||
return;
|
||||
}
|
||||
|
||||
final userSectorBox = Hive.box<UserSectorModel>(AppKeys.userSectorBoxName);
|
||||
debugPrint('Box UserSector contient ${userSectorBox.length} entrées au total');
|
||||
|
||||
// Afficher toutes les entrées pour debug
|
||||
for (var i = 0; i < userSectorBox.length; i++) {
|
||||
final us = userSectorBox.getAt(i);
|
||||
if (us != null) {
|
||||
debugPrint(' - UserSector[$i]: membreId=${us.id}, fkSector=${us.fkSector}, name="${us.firstName} ${us.name}"');
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer tous les UserSectorModel pour ce secteur
|
||||
final userSectors = userSectorBox.values
|
||||
.where((us) => us.fkSector == widget.existingSector!.id)
|
||||
.toList();
|
||||
|
||||
debugPrint('Trouvé ${userSectors.length} UserSectorModel pour le secteur ${widget.existingSector!.id}');
|
||||
|
||||
// Pré-sélectionner les IDs des membres affectés
|
||||
setState(() {
|
||||
_selectedMemberIds.clear();
|
||||
for (final userSector in userSectors) {
|
||||
// userSector.id est l'ID du membre (pas de l'utilisateur)
|
||||
_selectedMemberIds.add(userSector.id);
|
||||
debugPrint('Membre présélectionné: ${userSector.firstName} ${userSector.name} (membreId: ${userSector.id}, fkSector: ${userSector.fkSector})');
|
||||
}
|
||||
});
|
||||
|
||||
debugPrint('=== Fin chargement: ${_selectedMemberIds.length} membres présélectionnés ===');
|
||||
debugPrint('IDs présélectionnés: $_selectedMemberIds');
|
||||
|
||||
// Marquer le chargement comme terminé
|
||||
setState(() {
|
||||
_membersLoaded = true;
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du chargement des membres du secteur: $e');
|
||||
setState(() {
|
||||
_membersLoaded = true; // Même en cas d'erreur
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_nameFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Color _hexToColor(String hexColor) {
|
||||
final String colorStr = hexColor.startsWith('#') ? hexColor.substring(1) : hexColor;
|
||||
final String fullColorStr = colorStr.length == 6 ? 'FF$colorStr' : colorStr;
|
||||
return Color(int.parse(fullColorStr, radix: 16));
|
||||
}
|
||||
|
||||
String _colorToHex(Color color) {
|
||||
return '#${color.value.toRadixString(16).substring(2).toUpperCase()}';
|
||||
}
|
||||
|
||||
void _handleSave() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Vérifier qu'au moins un membre est sélectionné
|
||||
if (_selectedMemberIds.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Veuillez sélectionner au moins un membre'),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Indiquer que nous sommes en train de sauvegarder
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
// Appeler le callback onSave et attendre sa résolution
|
||||
await widget.onSave(
|
||||
_nameController.text.trim(),
|
||||
_colorToHex(_selectedColor),
|
||||
_selectedMemberIds,
|
||||
);
|
||||
|
||||
// Si tout s'est bien passé, fermer le dialog
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
// En cas d'erreur, réactiver le bouton
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
// L'erreur sera gérée par le callback onSave
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showColorPicker() {
|
||||
// Liste de couleurs prédéfinies
|
||||
final List<Color> colors = [
|
||||
Colors.red,
|
||||
Colors.pink,
|
||||
Colors.purple,
|
||||
Colors.deepPurple,
|
||||
Colors.indigo,
|
||||
Colors.blue,
|
||||
Colors.lightBlue,
|
||||
Colors.cyan,
|
||||
Colors.teal,
|
||||
Colors.green,
|
||||
Colors.lightGreen,
|
||||
Colors.lime,
|
||||
Colors.yellow,
|
||||
Colors.amber,
|
||||
Colors.orange,
|
||||
Colors.deepOrange,
|
||||
Colors.brown,
|
||||
Colors.grey,
|
||||
Colors.blueGrey,
|
||||
const Color(0xFF1E88E5), // Bleu personnalisé
|
||||
const Color(0xFF43A047), // Vert personnalisé
|
||||
const Color(0xFFE53935), // Rouge personnalisé
|
||||
const Color(0xFFFFB300), // Ambre personnalisé
|
||||
const Color(0xFF8E24AA), // Violet personnalisé
|
||||
];
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Choisir une couleur'),
|
||||
content: Container(
|
||||
width: double.maxFinite,
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
),
|
||||
itemCount: colors.length,
|
||||
itemBuilder: (context, index) {
|
||||
final color = colors[index];
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedColor = color;
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
border: Border.all(
|
||||
color: _selectedColor == color ? Colors.black : Colors.grey,
|
||||
width: _selectedColor == color ? 3 : 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentAmicale = CurrentAmicaleService.instance.currentAmicale;
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(widget.existingSector == null ? 'Nouveau secteur' : 'Modifier le secteur'),
|
||||
content: SingleChildScrollView(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Nom du secteur
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
focusNode: _nameFocusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nom du secteur',
|
||||
labelStyle: TextStyle(color: Colors.black),
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Nom du secteur'),
|
||||
Text(
|
||||
' *',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
hintText: 'Ex: Centre-ville',
|
||||
prefixIcon: Icon(Icons.location_on),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Veuillez entrer un nom';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Couleur du secteur
|
||||
const Text(
|
||||
'Couleur du secteur',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
_showColorPicker();
|
||||
},
|
||||
child: Container(
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: _selectedColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Toucher pour changer',
|
||||
style: TextStyle(
|
||||
color: _selectedColor.computeLuminance() > 0.5
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Sélection des membres
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'Membres affectés',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'*',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (_selectedMemberIds.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
'Sélectionnez au moins un membre',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (currentAmicale != null)
|
||||
ValueListenableBuilder<Box<MembreModel>>(
|
||||
valueListenable: Hive.box<MembreModel>(AppKeys.membresBoxName).listenable(),
|
||||
builder: (context, box, _) {
|
||||
debugPrint('=== Build liste membres - IDs présélectionnés: $_selectedMemberIds ===');
|
||||
|
||||
final membres = box.values
|
||||
.where((m) => m.fkEntite == currentAmicale.id)
|
||||
.toList();
|
||||
|
||||
if (membres.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('Aucun membre disponible'),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: membres.length,
|
||||
itemBuilder: (context, index) {
|
||||
final membre = membres[index];
|
||||
final isSelected = _selectedMemberIds.contains(membre.id);
|
||||
|
||||
// Log pour debug
|
||||
if (index < 3) { // Limiter les logs aux 3 premiers membres
|
||||
debugPrint('Membre ${index}: ${membre.firstName} ${membre.name} (ID: ${membre.id}) - isSelected: $isSelected');
|
||||
}
|
||||
|
||||
return CheckboxListTile(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 0.0),
|
||||
title: Text(
|
||||
'${membre.firstName} ${membre.name}${membre.sectName != null && membre.sectName!.isNotEmpty ? ' (${membre.sectName})' : ''}',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
value: isSelected,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
if (value == true) {
|
||||
_selectedMemberIds.add(membre.id);
|
||||
} else {
|
||||
_selectedMemberIds.remove(membre.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleSave,
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(widget.existingSector == null ? 'Créer' : 'Modifier'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
0
app/lib/presentation/public/landing_page.dart
Normal file → Executable file
0
app/lib/presentation/public/landing_page.dart
Normal file → Executable file
341
app/lib/presentation/settings/theme_settings_page.dart
Executable file
341
app/lib/presentation/settings/theme_settings_page.dart
Executable file
@@ -0,0 +1,341 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/core/services/theme_service.dart';
|
||||
import 'package:geosector_app/presentation/widgets/theme_switcher.dart';
|
||||
|
||||
/// Page de paramètres pour la gestion du thème
|
||||
class ThemeSettingsPage extends StatelessWidget {
|
||||
const ThemeSettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Paramètres d\'affichage'),
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
foregroundColor: theme.colorScheme.onSurface,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Section informations
|
||||
_buildInfoSection(context),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Section sélection du thème
|
||||
_buildThemeSection(context),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Section aperçu
|
||||
_buildPreviewSection(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoSection(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'À propos des thèmes',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'• Mode Automatique : Suit les préférences de votre système\n'
|
||||
'• Mode Clair : Interface claire en permanence\n'
|
||||
'• Mode Sombre : Interface sombre en permanence\n\n'
|
||||
'Le mode automatique détecte automatiquement si votre appareil '
|
||||
'est configuré en mode sombre ou clair et adapte l\'interface en conséquence.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThemeSection(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.palette_outlined, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Choix du thème',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Thème actuel
|
||||
AnimatedBuilder(
|
||||
animation: ThemeService.instance,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.primary.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
ThemeService.instance.themeModeIcon,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Thème actuel',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
ThemeService.instance.themeModeDescription,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Boutons de sélection style segments
|
||||
Text(
|
||||
'Sélectionner un thème :',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Center(
|
||||
child: ThemeSwitcher(
|
||||
style: ThemeSwitcherStyle.segmentedButton,
|
||||
onThemeChanged: () {
|
||||
// Optionnel: feedback haptic ou autres actions
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Thème changé vers ${ThemeService.instance.themeModeDescription}'),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Options alternatives
|
||||
Text(
|
||||
'Autres options :',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Dropdown
|
||||
const Row(
|
||||
children: [
|
||||
Text('Menu déroulant : '),
|
||||
ThemeSwitcher(
|
||||
style: ThemeSwitcherStyle.dropdown,
|
||||
showLabel: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Toggle buttons
|
||||
const Row(
|
||||
children: [
|
||||
Text('Boutons : '),
|
||||
ThemeSwitcher(style: ThemeSwitcherStyle.toggleButtons),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPreviewSection(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.preview, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Aperçu des couleurs',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Grille de couleurs
|
||||
GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
children: [
|
||||
_buildColorSample('Primary', theme.colorScheme.primary,
|
||||
theme.colorScheme.onPrimary),
|
||||
_buildColorSample('Secondary', theme.colorScheme.secondary,
|
||||
theme.colorScheme.onSecondary),
|
||||
_buildColorSample('Surface', theme.colorScheme.surface,
|
||||
theme.colorScheme.onSurface),
|
||||
_buildColorSample('Background', theme.colorScheme.surface,
|
||||
theme.colorScheme.onSurface),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Exemples de composants
|
||||
Text(
|
||||
'Exemples de composants :',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Bouton'),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Bouton'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('Bouton'),
|
||||
),
|
||||
const Chip(
|
||||
label: Text('Chip'),
|
||||
avatar: Icon(Icons.star, size: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildColorSample(String label, Color color, Color onColor) {
|
||||
return Container(
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.3)),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: onColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dialog simple pour les paramètres de thème
|
||||
class ThemeSettingsDialog extends StatelessWidget {
|
||||
const ThemeSettingsDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.palette_outlined, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Apparence'),
|
||||
],
|
||||
),
|
||||
content: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ThemeInfo(),
|
||||
SizedBox(height: 16),
|
||||
ThemeSwitcher(
|
||||
style: ThemeSwitcherStyle.segmentedButton,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
0
app/lib/presentation/user/user_communication_page.dart
Normal file → Executable file
0
app/lib/presentation/user/user_communication_page.dart
Normal file → Executable file
6
app/lib/presentation/user/user_dashboard_home_page.dart
Normal file → Executable file
6
app/lib/presentation/user/user_dashboard_home_page.dart
Normal file → Executable file
@@ -241,8 +241,8 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
|
||||
|
||||
/// Récupère les passages récents pour la liste
|
||||
List<Map<String, dynamic>> _getRecentPassages(Box<PassageModel> passagesBox) {
|
||||
final allPassages = passagesBox.values.toList();
|
||||
allPassages.sort((a, b) => b.passedAt.compareTo(a.passedAt));
|
||||
final allPassages = passagesBox.values.where((p) => p.passedAt != null).toList();
|
||||
allPassages.sort((a, b) => b.passedAt!.compareTo(a.passedAt!));
|
||||
|
||||
// Limiter aux 10 passages les plus récents
|
||||
final recentPassagesModels = allPassages.take(10).toList();
|
||||
@@ -270,7 +270,7 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
|
||||
'id': passage.id, // Garder l'ID comme int, pas besoin de toString()
|
||||
'address': address,
|
||||
'amount': amount,
|
||||
'date': passage.passedAt,
|
||||
'date': passage.passedAt ?? DateTime.now(),
|
||||
'type': passage.fkType,
|
||||
'payment': passage.fkTypeReglement,
|
||||
'name': passage.name,
|
||||
|
||||
3
app/lib/presentation/user/user_dashboard_page.dart
Normal file → Executable file
3
app/lib/presentation/user/user_dashboard_page.dart
Normal file → Executable file
@@ -1,9 +1,6 @@
|
||||
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
||||
import 'package:flutter/material.dart';
|
||||
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';
|
||||
|
||||
100
app/lib/presentation/user/user_history_page.dart
Normal file → Executable file
100
app/lib/presentation/user/user_history_page.dart
Normal file → Executable file
@@ -1,10 +1,8 @@
|
||||
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
|
||||
// Pour accéder aux instances globales
|
||||
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/core/repositories/passage_repository.dart';
|
||||
import 'package:geosector_app/core/repositories/user_repository.dart';
|
||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||
|
||||
class UserHistoryPage extends StatefulWidget {
|
||||
@@ -71,46 +69,52 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
|
||||
// Afficher la plage de dates pour le débogage
|
||||
if (filtered.isNotEmpty) {
|
||||
// Trier par date pour trouver min et max
|
||||
final sortedByDate = List<PassageModel>.from(filtered);
|
||||
sortedByDate.sort((a, b) => a.passedAt.compareTo(b.passedAt));
|
||||
// Trier par date pour trouver min et max (exclure les passages sans date)
|
||||
final sortedByDate =
|
||||
List<PassageModel>.from(filtered.where((p) => p.passedAt != null));
|
||||
if (sortedByDate.isNotEmpty) {
|
||||
sortedByDate.sort((a, b) => a.passedAt!.compareTo(b.passedAt!));
|
||||
|
||||
final DateTime minDate = sortedByDate.first.passedAt;
|
||||
final DateTime maxDate = sortedByDate.last.passedAt;
|
||||
final DateTime minDate = sortedByDate.first.passedAt!;
|
||||
final DateTime maxDate = sortedByDate.last.passedAt!;
|
||||
|
||||
// Log détaillé pour débogage
|
||||
debugPrint(
|
||||
'Plage de dates des passages: ${minDate.toString()} à ${maxDate.toString()}');
|
||||
|
||||
// Afficher les 5 passages les plus anciens et les 5 plus récents pour débogage
|
||||
debugPrint('\n--- 5 PASSAGES LES PLUS ANCIENS ---');
|
||||
for (int i = 0; i < sortedByDate.length && i < 5; i++) {
|
||||
final p = sortedByDate[i];
|
||||
// Log détaillé pour débogage
|
||||
debugPrint(
|
||||
'ID: ${p.id}, Type: ${p.fkType}, Date: ${p.passedAt}, Adresse: ${p.rue}');
|
||||
}
|
||||
'Plage de dates des passages: ${minDate.toString()} à ${maxDate.toString()}');
|
||||
|
||||
debugPrint('\n--- 5 PASSAGES LES PLUS RÉCENTS ---');
|
||||
for (int i = sortedByDate.length - 1;
|
||||
i >= 0 && i >= sortedByDate.length - 5;
|
||||
i--) {
|
||||
final p = sortedByDate[i];
|
||||
debugPrint(
|
||||
'ID: ${p.id}, Type: ${p.fkType}, Date: ${p.passedAt}, Adresse: ${p.rue}');
|
||||
}
|
||||
// Afficher les 5 passages les plus anciens et les 5 plus récents pour débogage
|
||||
debugPrint('\n--- 5 PASSAGES LES PLUS ANCIENS ---');
|
||||
for (int i = 0; i < sortedByDate.length && i < 5; i++) {
|
||||
final p = sortedByDate[i];
|
||||
debugPrint(
|
||||
'ID: ${p.id}, Type: ${p.fkType}, Date: ${p.passedAt}, Adresse: ${p.rue}');
|
||||
}
|
||||
|
||||
// Vérifier la distribution des passages par mois
|
||||
final Map<String, int> monthCount = {};
|
||||
for (var passage in filtered) {
|
||||
final String monthKey =
|
||||
'${passage.passedAt.year}-${passage.passedAt.month.toString().padLeft(2, '0')}';
|
||||
monthCount[monthKey] = (monthCount[monthKey] ?? 0) + 1;
|
||||
}
|
||||
debugPrint('\n--- 5 PASSAGES LES PLUS RÉCENTS ---');
|
||||
for (int i = sortedByDate.length - 1;
|
||||
i >= 0 && i >= sortedByDate.length - 5;
|
||||
i--) {
|
||||
final p = sortedByDate[i];
|
||||
debugPrint(
|
||||
'ID: ${p.id}, Type: ${p.fkType}, Date: ${p.passedAt}, Adresse: ${p.rue}');
|
||||
}
|
||||
|
||||
debugPrint('\n--- DISTRIBUTION PAR MOIS ---');
|
||||
final sortedMonths = monthCount.keys.toList()..sort();
|
||||
for (var month in sortedMonths) {
|
||||
debugPrint('$month: ${monthCount[month]} passages');
|
||||
// Vérifier la distribution des passages par mois
|
||||
final Map<String, int> monthCount = {};
|
||||
for (var passage in filtered) {
|
||||
// Ignorer les passages sans date
|
||||
if (passage.passedAt != null) {
|
||||
final String monthKey =
|
||||
'${passage.passedAt!.year}-${passage.passedAt!.month.toString().padLeft(2, '0')}';
|
||||
monthCount[monthKey] = (monthCount[monthKey] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('\n--- DISTRIBUTION PAR MOIS ---');
|
||||
final sortedMonths = monthCount.keys.toList()..sort();
|
||||
for (var month in sortedMonths) {
|
||||
debugPrint('$month: ${monthCount[month]} passages');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,9 +124,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
try {
|
||||
final Map<String, dynamic> passageMap =
|
||||
_convertPassageModelToMap(passage);
|
||||
if (passageMap != null) {
|
||||
passagesMap.add(passageMap);
|
||||
}
|
||||
passagesMap.add(passageMap);
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la conversion du passage en map: $e');
|
||||
// Ignorer ce passage et continuer
|
||||
@@ -195,7 +197,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
// Récupérer la date avec gestion d'erreur
|
||||
DateTime date;
|
||||
try {
|
||||
date = passage.passedAt;
|
||||
date = passage.passedAt ?? DateTime.now();
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la récupération de la date: $e');
|
||||
date = DateTime.now();
|
||||
@@ -353,7 +355,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Détails du passage'),
|
||||
title: const Text('Détails du passage'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -379,7 +381,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('Fermer'),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
if (passage['hasReceipt'] == true)
|
||||
TextButton(
|
||||
@@ -387,14 +389,14 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
Navigator.of(context).pop();
|
||||
_showReceipt(passage);
|
||||
},
|
||||
child: Text('Voir le reçu'),
|
||||
child: const Text('Voir le reçu'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_editPassage(passage);
|
||||
},
|
||||
child: Text('Modifier'),
|
||||
child: const Text('Modifier'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -427,11 +429,11 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text('$label:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold))),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: isError ? TextStyle(color: Colors.red) : null,
|
||||
style: isError ? const TextStyle(color: Colors.red) : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -440,7 +442,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
}
|
||||
|
||||
// Variable pour gérer la recherche
|
||||
String _searchQuery = '';
|
||||
final String _searchQuery = '';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -533,7 +535,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
|
||||
'Tous', // Toujours commencer avec 'Tous' pour voir tous les types
|
||||
initialPaymentFilter: 'Tous',
|
||||
// Exclure les passages de type 2 (À finaliser)
|
||||
excludePassageTypes: [2],
|
||||
excludePassageTypes: const [2],
|
||||
// Filtrer par utilisateur courant
|
||||
filterByUserId: userRepository.getCurrentUser()?.id,
|
||||
// Désactiver les filtres de date implicites
|
||||
|
||||
49
app/lib/presentation/user/user_map_page.dart
Normal file → Executable file
49
app/lib/presentation/user/user_map_page.dart
Normal file → Executable file
@@ -634,7 +634,7 @@ class _UserMapPageState extends State<UserMapPage> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.location_on,
|
||||
const Icon(Icons.location_on,
|
||||
size: 18, color: Colors.blue),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
@@ -644,7 +644,7 @@ class _UserMapPageState extends State<UserMapPage> {
|
||||
isExpanded: true,
|
||||
underline:
|
||||
Container(), // Supprimer la ligne sous le dropdown
|
||||
icon: Icon(Icons.arrow_drop_down,
|
||||
icon: const Icon(Icons.arrow_drop_down,
|
||||
color: Colors.blue),
|
||||
items: _sectorItems,
|
||||
onChanged: (int? sectorId) {
|
||||
@@ -879,10 +879,17 @@ class _UserMapPageState extends State<UserMapPage> {
|
||||
// Construire les marqueurs pour les passages
|
||||
List<Marker> _buildPassageMarkers() {
|
||||
return _passages.map((passage) {
|
||||
final PassageModel passageModel = passage['model'] as PassageModel;
|
||||
final bool hasNoSector = passageModel.fkSector == null;
|
||||
|
||||
// Si le passage n'a pas de secteur, on met une bordure rouge épaisse
|
||||
final Color borderColor = hasNoSector ? Colors.red : Colors.white;
|
||||
final double borderWidth = hasNoSector ? 3.0 : 1.0;
|
||||
|
||||
return Marker(
|
||||
point: passage['position'] as LatLng,
|
||||
width: 14.0,
|
||||
height: 14.0,
|
||||
width: hasNoSector ? 18.0 : 14.0, // Plus grand si orphelin
|
||||
height: hasNoSector ? 18.0 : 14.0,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_showPassageInfo(passage);
|
||||
@@ -892,8 +899,8 @@ class _UserMapPageState extends State<UserMapPage> {
|
||||
color: passage['color'] as Color,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 1.0,
|
||||
color: borderColor,
|
||||
width: borderWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -958,10 +965,10 @@ class _UserMapPageState extends State<UserMapPage> {
|
||||
}
|
||||
}
|
||||
|
||||
// Formater la date (uniquement si le type n'est pas 2)
|
||||
// Formater la date (uniquement si le type n'est pas 2 et si la date existe)
|
||||
String dateInfo = '';
|
||||
if (type != 2) {
|
||||
dateInfo = 'Date: ${_formatDate(passageModel.passedAt)}';
|
||||
if (type != 2 && passageModel.passedAt != null) {
|
||||
dateInfo = 'Date: ${_formatDate(passageModel.passedAt!)}';
|
||||
}
|
||||
|
||||
// Récupérer le nom du passage (si le type n'est pas 6 - Maison vide)
|
||||
@@ -1008,6 +1015,30 @@ class _UserMapPageState extends State<UserMapPage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Afficher en premier si le passage n'est pas affecté à un secteur
|
||||
if (passageModel.fkSector == null) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.1),
|
||||
border: Border.all(color: Colors.red, width: 1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.warning, color: Colors.red, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Ce passage n\'est plus affecté à un secteur',
|
||||
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
Text('Adresse: $adresse'),
|
||||
if (residenceInfo != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
|
||||
46
app/lib/presentation/user/user_statistics_page.dart
Normal file → Executable file
46
app/lib/presentation/user/user_statistics_page.dart
Normal file → Executable file
@@ -56,8 +56,7 @@ class _UserStatisticsPageState extends State<UserStatisticsPage> {
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Résumé par type de règlement
|
||||
_buildPaymentTypeSummary(theme, isDesktop),
|
||||
|
||||
_buildPaymentTypeSummary(theme, isDesktop),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -219,17 +218,17 @@ _buildPaymentTypeSummary(theme, isDesktop),
|
||||
onChanged(selection.first);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||
(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
backgroundColor: WidgetStateProperty.resolveWith<Color>(
|
||||
(Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return AppTheme.secondaryColor;
|
||||
}
|
||||
return theme.colorScheme.surface;
|
||||
},
|
||||
),
|
||||
foregroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||
(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
foregroundColor: WidgetStateProperty.resolveWith<Color>(
|
||||
(Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return Colors.white;
|
||||
}
|
||||
return theme.colorScheme.onSurface;
|
||||
@@ -375,20 +374,19 @@ _buildPaymentTypeSummary(theme, isDesktop),
|
||||
}
|
||||
|
||||
// Construction du résumé par type de règlement
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
0
app/lib/presentation/widgets/amicale_form.dart
Normal file → Executable file
0
app/lib/presentation/widgets/amicale_form.dart
Normal file → Executable file
0
app/lib/presentation/widgets/amicale_row_widget.dart
Normal file → Executable file
0
app/lib/presentation/widgets/amicale_row_widget.dart
Normal file → Executable file
0
app/lib/presentation/widgets/amicale_table_widget.dart
Normal file → Executable file
0
app/lib/presentation/widgets/amicale_table_widget.dart
Normal file → Executable file
6
app/lib/presentation/widgets/charts/activity_chart.dart
Normal file → Executable file
6
app/lib/presentation/widgets/charts/activity_chart.dart
Normal file → Executable file
@@ -225,11 +225,13 @@ class _ActivityChartState extends State<ActivityChart>
|
||||
|
||||
// Vérifier si le passage est dans la période
|
||||
final passageDate = passage.passedAt;
|
||||
if (passageDate.isBefore(startDate) || passageDate.isAfter(endDate)) {
|
||||
if (passageDate == null ||
|
||||
passageDate.isBefore(startDate) ||
|
||||
passageDate.isAfter(endDate)) {
|
||||
shouldInclude = false;
|
||||
}
|
||||
|
||||
if (shouldInclude) {
|
||||
if (shouldInclude && passageDate != null) {
|
||||
final dateStr = DateFormat('yyyy-MM-dd').format(passageDate);
|
||||
if (dataByDate.containsKey(dateStr)) {
|
||||
dataByDate[dateStr]![passage.fkType] =
|
||||
|
||||
0
app/lib/presentation/widgets/charts/charts.dart
Normal file → Executable file
0
app/lib/presentation/widgets/charts/charts.dart
Normal file → Executable file
4
app/lib/presentation/widgets/charts/combined_chart.dart
Normal file → Executable file
4
app/lib/presentation/widgets/charts/combined_chart.dart
Normal file → Executable file
@@ -198,7 +198,7 @@ class CombinedChart extends StatelessWidget {
|
||||
reservedSize: 40,
|
||||
),
|
||||
),
|
||||
topTitles: AxisTitles(
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
),
|
||||
@@ -214,7 +214,7 @@ class CombinedChart extends StatelessWidget {
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
barGroups: _createBarGroups(allDates, passagesByType),
|
||||
extraLinesData: ExtraLinesData(
|
||||
extraLinesData: const ExtraLinesData(
|
||||
horizontalLines: [],
|
||||
verticalLines: [],
|
||||
extraLinesOnTop: true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user