refactor: Simplifier DashboardLayout/AppScaffold (tâche #74)
Centralisation et simplification de l'architecture de navigation : CRÉATIONS : - navigation_config.dart : Configuration centralisée de la navigation * Toutes les destinations (admin/user) * Logique de navigation (index → route) * Résolution inverse (route → index) * Titres et utilitaires - backgrounds/dots_painter.dart : Painter de points décoratifs * Extrait depuis AppScaffold et AdminScaffold * Paramétrable (opacité, densité, seed) * Réutilisable - backgrounds/gradient_background.dart : Fond dégradé * Gère les couleurs admin (rouge) / user (vert) * Option pour afficher/masquer les points * Widget indépendant SIMPLIFICATIONS : - app_scaffold.dart : 426 → 192 lignes (-55%) * Utilise NavigationConfig au lieu de NavigationHelper * Utilise GradientBackground au lieu de code dupliqué * Suppression de DotsPainter local - dashboard_layout.dart : 140 → 77 lignes (-45%) * Suppression validations excessives (try/catch, vérifications) * Code épuré et plus lisible SUPPRESSIONS : - admin_scaffold.dart : Supprimé (207 lignes) * Obsolète depuis unification avec AppScaffold * Code dupliqué avec AppScaffold * AdminNavigationHelper fusionné dans NavigationConfig RÉSULTATS : - Avant : 773 lignes (AppScaffold + AdminScaffold + DashboardLayout) - Après : 623 lignes (tout inclus) - Réduction nette : -150 lignes (-19%) - Architecture plus claire et maintenable - Aucune duplication de code - Navigation centralisée en un seul endroit Résout tâche #74 du PLANNING-2026-Q1.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
259
app/lib/core/config/navigation_config.dart
Executable file
259
app/lib/core/config/navigation_config.dart
Executable file
@@ -0,0 +1,259 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:geosector_app/presentation/widgets/badged_navigation_destination.dart';
|
||||||
|
|
||||||
|
/// Configuration centralisée de la navigation pour toute l'application
|
||||||
|
/// Gère les destinations, routes et logique de navigation pour admin et user
|
||||||
|
class NavigationConfig {
|
||||||
|
// ========================================
|
||||||
|
// DESTINATIONS DE NAVIGATION
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// Destinations communes à tous les rôles
|
||||||
|
static const List<NavigationDestination> _commonDestinations = [
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.dashboard_outlined),
|
||||||
|
selectedIcon: Icon(Icons.dashboard),
|
||||||
|
label: 'Tableau de bord',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.history_outlined),
|
||||||
|
selectedIcon: Icon(Icons.history),
|
||||||
|
label: 'Historique',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.map_outlined),
|
||||||
|
selectedIcon: Icon(Icons.map),
|
||||||
|
label: 'Carte',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Destination Messages avec badge (commune à tous)
|
||||||
|
static NavigationDestination get _messagesDestination {
|
||||||
|
return createBadgedNavigationDestination(
|
||||||
|
icon: const Icon(Icons.chat_outlined),
|
||||||
|
selectedIcon: const Icon(Icons.chat),
|
||||||
|
label: 'Messages',
|
||||||
|
showBadge: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Destination Terrain (user uniquement)
|
||||||
|
static const NavigationDestination _fieldModeDestination = NavigationDestination(
|
||||||
|
icon: Icon(Icons.explore_outlined),
|
||||||
|
selectedIcon: Icon(Icons.explore),
|
||||||
|
label: 'Terrain',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Destinations admin desktop uniquement
|
||||||
|
static const List<NavigationDestination> _adminDesktopDestinations = [
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.business_outlined),
|
||||||
|
selectedIcon: Icon(Icons.business),
|
||||||
|
label: 'Amicale & membres',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.calendar_today_outlined),
|
||||||
|
selectedIcon: Icon(Icons.calendar_today),
|
||||||
|
label: 'Opérations',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.analytics_outlined),
|
||||||
|
selectedIcon: Icon(Icons.analytics),
|
||||||
|
label: 'Connexions',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// GETTERS DE DESTINATIONS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// Obtenir la liste des destinations selon le rôle et le device
|
||||||
|
static List<NavigationDestination> getDestinations({
|
||||||
|
required bool isAdmin,
|
||||||
|
required bool isMobile,
|
||||||
|
}) {
|
||||||
|
final destinations = <NavigationDestination>[];
|
||||||
|
|
||||||
|
// Ajouter les destinations communes
|
||||||
|
destinations.addAll(_commonDestinations);
|
||||||
|
|
||||||
|
// Ajouter Messages (avec badge)
|
||||||
|
destinations.add(_messagesDestination);
|
||||||
|
|
||||||
|
if (isAdmin) {
|
||||||
|
// Admin : ajouter les pages supplémentaires sur desktop uniquement
|
||||||
|
if (!isMobile) {
|
||||||
|
destinations.addAll(_adminDesktopDestinations);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// User : ajouter la page Terrain
|
||||||
|
destinations.add(_fieldModeDestination);
|
||||||
|
}
|
||||||
|
|
||||||
|
return destinations;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// NAVIGATION (INDEX → ROUTE)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// Naviguer vers une page selon l'index et le rôle
|
||||||
|
static void navigateToIndex(BuildContext context, int index, bool isAdmin) {
|
||||||
|
if (isAdmin) {
|
||||||
|
_navigateAdminIndex(context, index);
|
||||||
|
} else {
|
||||||
|
_navigateUserIndex(context, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigation pour les admins
|
||||||
|
static void _navigateAdminIndex(BuildContext context, int index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
context.go('/admin');
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
context.go('/admin/history');
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
context.go('/admin/map');
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
context.go('/admin/messages');
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
context.go('/admin/amicale');
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
context.go('/admin/operations');
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
context.go('/admin/connexions');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
context.go('/admin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigation pour les utilisateurs standards
|
||||||
|
static void _navigateUserIndex(BuildContext context, int index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
context.go('/user/dashboard');
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
context.go('/user/history');
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
context.go('/user/map');
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
context.go('/user/messages');
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
context.go('/user/field-mode');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
context.go('/user/dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// RÉSOLUTION (ROUTE → INDEX)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// Obtenir l'index selon la route actuelle et le rôle
|
||||||
|
static int getIndexFromRoute(String route, bool isAdmin) {
|
||||||
|
// Enlever les paramètres de query si présents
|
||||||
|
final cleanRoute = route.split('?').first;
|
||||||
|
|
||||||
|
if (isAdmin) {
|
||||||
|
return _getAdminIndexFromRoute(cleanRoute);
|
||||||
|
} else {
|
||||||
|
return _getUserIndexFromRoute(cleanRoute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtenir l'index admin depuis la route
|
||||||
|
static int _getAdminIndexFromRoute(String route) {
|
||||||
|
if (route.contains('/admin/history')) return 1;
|
||||||
|
if (route.contains('/admin/map')) return 2;
|
||||||
|
if (route.contains('/admin/messages')) return 3;
|
||||||
|
if (route.contains('/admin/amicale')) return 4;
|
||||||
|
if (route.contains('/admin/operations')) return 5;
|
||||||
|
if (route.contains('/admin/connexions')) return 6;
|
||||||
|
return 0; // Dashboard par défaut
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtenir l'index user depuis la route
|
||||||
|
static int _getUserIndexFromRoute(String route) {
|
||||||
|
if (route.contains('/user/history')) return 1;
|
||||||
|
if (route.contains('/user/map')) return 2;
|
||||||
|
if (route.contains('/user/messages')) return 3;
|
||||||
|
if (route.contains('/user/field-mode')) return 4;
|
||||||
|
return 0; // Dashboard par défaut
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// UTILITAIRES
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// Obtenir le nom de la page selon l'index et le rôle
|
||||||
|
static String getPageNameFromIndex(int index, bool isAdmin) {
|
||||||
|
if (isAdmin) {
|
||||||
|
return _getAdminPageName(index);
|
||||||
|
} else {
|
||||||
|
return _getUserPageName(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtenir le nom de page admin
|
||||||
|
static String _getAdminPageName(int index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
return 'dashboard';
|
||||||
|
case 1:
|
||||||
|
return 'history';
|
||||||
|
case 2:
|
||||||
|
return 'map';
|
||||||
|
case 3:
|
||||||
|
return 'messages';
|
||||||
|
case 4:
|
||||||
|
return 'amicale';
|
||||||
|
case 5:
|
||||||
|
return 'operations';
|
||||||
|
case 6:
|
||||||
|
return 'connexions';
|
||||||
|
default:
|
||||||
|
return 'dashboard';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtenir le nom de page user
|
||||||
|
static String _getUserPageName(int index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
return 'dashboard';
|
||||||
|
case 1:
|
||||||
|
return 'history';
|
||||||
|
case 2:
|
||||||
|
return 'map';
|
||||||
|
case 3:
|
||||||
|
return 'messages';
|
||||||
|
case 4:
|
||||||
|
return 'field-mode';
|
||||||
|
default:
|
||||||
|
return 'dashboard';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// TITRES ET LABELS
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
/// Obtenir le titre du dashboard selon le rôle
|
||||||
|
static String getDashboardTitle(bool isAdmin) {
|
||||||
|
return isAdmin ? 'Tableau de bord Administration' : 'GEOSECTOR';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:geosector_app/presentation/widgets/dashboard_layout.dart';
|
|
||||||
import 'package:geosector_app/presentation/widgets/badged_navigation_destination.dart';
|
|
||||||
import 'package:geosector_app/app.dart';
|
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
/// Class pour dessiner les petits points blancs sur le fond
|
|
||||||
class DotsPainter extends CustomPainter {
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
final paint = Paint()
|
|
||||||
..color = Colors.white.withOpacity(0.5)
|
|
||||||
..style = PaintingStyle.fill;
|
|
||||||
|
|
||||||
final random = math.Random(42); // Seed fixe pour consistance
|
|
||||||
final numberOfDots = (size.width * size.height) ~/ 1500;
|
|
||||||
|
|
||||||
for (int i = 0; i < numberOfDots; i++) {
|
|
||||||
final x = random.nextDouble() * size.width;
|
|
||||||
final y = random.nextDouble() * size.height;
|
|
||||||
final radius = 1.0 + random.nextDouble() * 2.0;
|
|
||||||
canvas.drawCircle(Offset(x, y), radius, paint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scaffold partagé pour toutes les pages d'administration
|
|
||||||
/// Fournit le fond dégradé et la navigation commune
|
|
||||||
class AdminScaffold extends StatelessWidget {
|
|
||||||
/// Le contenu de la page
|
|
||||||
final Widget body;
|
|
||||||
|
|
||||||
/// L'index de navigation sélectionné
|
|
||||||
final int selectedIndex;
|
|
||||||
|
|
||||||
/// Le titre de la page
|
|
||||||
final String pageTitle;
|
|
||||||
|
|
||||||
/// Callback optionnel pour gérer la navigation personnalisée
|
|
||||||
final Function(int)? onDestinationSelected;
|
|
||||||
|
|
||||||
const AdminScaffold({
|
|
||||||
super.key,
|
|
||||||
required this.body,
|
|
||||||
required this.selectedIndex,
|
|
||||||
required this.pageTitle,
|
|
||||||
this.onDestinationSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final currentUser = userRepository.getCurrentUser();
|
|
||||||
final size = MediaQuery.of(context).size;
|
|
||||||
final isMobile = size.width <= 900;
|
|
||||||
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
// Fond dégradé avec petits points blancs
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [Colors.white, Colors.red.shade300],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: CustomPaint(
|
|
||||||
painter: DotsPainter(),
|
|
||||||
child: const SizedBox(width: double.infinity, height: double.infinity),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Contenu de la page avec navigation
|
|
||||||
DashboardLayout(
|
|
||||||
key: ValueKey('dashboard_layout_$selectedIndex'),
|
|
||||||
title: 'Tableau de bord Administration',
|
|
||||||
selectedIndex: selectedIndex,
|
|
||||||
onDestinationSelected: onDestinationSelected ?? (index) {
|
|
||||||
// Navigation par défaut si pas de callback personnalisé
|
|
||||||
AdminNavigationHelper.navigateToIndex(context, index);
|
|
||||||
},
|
|
||||||
destinations: AdminNavigationHelper.getDestinations(
|
|
||||||
currentUser: currentUser,
|
|
||||||
isMobile: isMobile,
|
|
||||||
),
|
|
||||||
isAdmin: true,
|
|
||||||
body: body,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper pour centraliser la logique de navigation admin
|
|
||||||
class AdminNavigationHelper {
|
|
||||||
/// Obtenir la liste des destinations de navigation selon le rôle et le device
|
|
||||||
static List<NavigationDestination> getDestinations({
|
|
||||||
required dynamic currentUser,
|
|
||||||
required bool isMobile,
|
|
||||||
}) {
|
|
||||||
final destinations = <NavigationDestination>[
|
|
||||||
// Pages de base toujours visibles
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.dashboard_outlined),
|
|
||||||
selectedIcon: Icon(Icons.dashboard),
|
|
||||||
label: 'Tableau de bord',
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.bar_chart_outlined),
|
|
||||||
selectedIcon: Icon(Icons.bar_chart),
|
|
||||||
label: 'Statistiques',
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.history_outlined),
|
|
||||||
selectedIcon: Icon(Icons.history),
|
|
||||||
label: 'Historique',
|
|
||||||
),
|
|
||||||
createBadgedNavigationDestination(
|
|
||||||
icon: const Icon(Icons.chat_outlined),
|
|
||||||
selectedIcon: const Icon(Icons.chat),
|
|
||||||
label: 'Messages',
|
|
||||||
showBadge: true,
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.map_outlined),
|
|
||||||
selectedIcon: Icon(Icons.map),
|
|
||||||
label: 'Carte',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Ajouter les pages admin (role 2) seulement sur desktop
|
|
||||||
if (currentUser?.role == 2 && !isMobile) {
|
|
||||||
destinations.addAll([
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.business_outlined),
|
|
||||||
selectedIcon: Icon(Icons.business),
|
|
||||||
label: 'Amicale & membres',
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.calendar_today_outlined),
|
|
||||||
selectedIcon: Icon(Icons.calendar_today),
|
|
||||||
label: 'Opérations',
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return destinations;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Naviguer vers une page selon l'index
|
|
||||||
static void navigateToIndex(BuildContext context, int index) {
|
|
||||||
switch (index) {
|
|
||||||
case 0:
|
|
||||||
context.go('/admin');
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
context.go('/admin/statistics');
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
context.go('/admin/history');
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
context.go('/admin/messages');
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
context.go('/admin/map');
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
context.go('/admin/amicale');
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
context.go('/admin/operations');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
context.go('/admin');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtenir l'index selon la route actuelle
|
|
||||||
static int getIndexFromRoute(String route) {
|
|
||||||
if (route.contains('/statistics')) return 1;
|
|
||||||
if (route.contains('/history')) return 2;
|
|
||||||
if (route.contains('/messages')) return 3;
|
|
||||||
if (route.contains('/map')) return 4;
|
|
||||||
if (route.contains('/amicale')) return 5;
|
|
||||||
if (route.contains('/operations')) return 6;
|
|
||||||
return 0; // Dashboard par défaut
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtenir le nom de la page selon l'index
|
|
||||||
static String getPageNameFromIndex(int index) {
|
|
||||||
switch (index) {
|
|
||||||
case 0: return 'dashboard';
|
|
||||||
case 1: return 'statistics';
|
|
||||||
case 2: return 'history';
|
|
||||||
case 3: return 'messages';
|
|
||||||
case 4: return 'map';
|
|
||||||
case 5: return 'amicale';
|
|
||||||
case 6: return 'operations';
|
|
||||||
default: return 'dashboard';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
265
app/lib/presentation/widgets/app_scaffold.dart
Normal file → Executable file
265
app/lib/presentation/widgets/app_scaffold.dart
Normal file → Executable file
@@ -1,33 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:geosector_app/presentation/widgets/dashboard_layout.dart';
|
import 'package:geosector_app/presentation/widgets/dashboard_layout.dart';
|
||||||
import 'package:geosector_app/presentation/widgets/badged_navigation_destination.dart';
|
import 'package:geosector_app/presentation/widgets/backgrounds/gradient_background.dart';
|
||||||
|
import 'package:geosector_app/core/config/navigation_config.dart';
|
||||||
import 'package:geosector_app/core/services/current_user_service.dart';
|
import 'package:geosector_app/core/services/current_user_service.dart';
|
||||||
import 'package:geosector_app/app.dart';
|
import 'package:geosector_app/app.dart';
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
/// Classe pour dessiner les petits points blancs sur le fond
|
|
||||||
class DotsPainter extends CustomPainter {
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
final paint = Paint()
|
|
||||||
..color = Colors.white.withOpacity(0.5)
|
|
||||||
..style = PaintingStyle.fill;
|
|
||||||
|
|
||||||
final random = math.Random(42); // Seed fixe pour consistance
|
|
||||||
final numberOfDots = (size.width * size.height) ~/ 1500;
|
|
||||||
|
|
||||||
for (int i = 0; i < numberOfDots; i++) {
|
|
||||||
final x = random.nextDouble() * size.width;
|
|
||||||
final y = random.nextDouble() * size.height;
|
|
||||||
final radius = 1.0 + random.nextDouble() * 2.0;
|
|
||||||
canvas.drawCircle(Offset(x, y), radius, paint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scaffold unifié pour toutes les pages (admin et user)
|
/// Scaffold unifié pour toutes les pages (admin et user)
|
||||||
/// Adapte automatiquement son apparence selon le rôle de l'utilisateur
|
/// Adapte automatiquement son apparence selon le rôle de l'utilisateur
|
||||||
@@ -102,43 +78,26 @@ class AppScaffold extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Couleurs de fond selon le rôle
|
|
||||||
final gradientColors = isAdmin
|
|
||||||
? [Colors.white, Colors.red.shade300] // Admin: dégradé rouge
|
|
||||||
: [Colors.white, Colors.green.shade300]; // User: dégradé vert
|
|
||||||
|
|
||||||
// Titre avec suffixe selon le rôle
|
|
||||||
final dashboardTitle = isAdmin
|
|
||||||
? 'Tableau de bord Administration'
|
|
||||||
: 'GEOSECTOR';
|
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
// Fond dégradé avec petits points blancs (optionnel)
|
// Fond dégradé avec petits points blancs (optionnel)
|
||||||
if (showBackground)
|
if (showBackground)
|
||||||
Container(
|
GradientBackground(
|
||||||
decoration: BoxDecoration(
|
isAdmin: isAdmin,
|
||||||
gradient: LinearGradient(
|
showDots: true,
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: gradientColors,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: CustomPaint(
|
|
||||||
painter: DotsPainter(),
|
|
||||||
child: const SizedBox(width: double.infinity, height: double.infinity),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Contenu de la page avec navigation
|
// Contenu de la page avec navigation
|
||||||
DashboardLayout(
|
DashboardLayout(
|
||||||
key: ValueKey('dashboard_layout_${isAdmin ? 'admin' : 'user'}_$selectedIndex'),
|
key: ValueKey(
|
||||||
title: dashboardTitle,
|
'dashboard_layout_${isAdmin ? 'admin' : 'user'}_$selectedIndex'),
|
||||||
|
title: NavigationConfig.getDashboardTitle(isAdmin),
|
||||||
selectedIndex: selectedIndex,
|
selectedIndex: selectedIndex,
|
||||||
onDestinationSelected: onDestinationSelected ?? (index) {
|
onDestinationSelected: onDestinationSelected ??
|
||||||
NavigationHelper.navigateToIndex(context, index, isAdmin);
|
(index) {
|
||||||
},
|
NavigationConfig.navigateToIndex(context, index, isAdmin);
|
||||||
destinations: NavigationHelper.getDestinations(
|
},
|
||||||
|
destinations: NavigationConfig.getDestinations(
|
||||||
isAdmin: isAdmin,
|
isAdmin: isAdmin,
|
||||||
isMobile: isMobile,
|
isMobile: isMobile,
|
||||||
),
|
),
|
||||||
@@ -159,27 +118,13 @@ class AppScaffold extends StatelessWidget {
|
|||||||
}) {
|
}) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
// Utiliser le même fond que pour un utilisateur normal (vert)
|
|
||||||
final gradientColors = isAdmin
|
|
||||||
? [Colors.white, Colors.red.shade300]
|
|
||||||
: [Colors.white, Colors.green.shade300];
|
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
// Fond dégradé (optionnel)
|
// Fond dégradé (optionnel)
|
||||||
if (showBackground)
|
if (showBackground)
|
||||||
Container(
|
GradientBackground(
|
||||||
decoration: BoxDecoration(
|
isAdmin: isAdmin,
|
||||||
gradient: LinearGradient(
|
showDots: true,
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: gradientColors,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: CustomPaint(
|
|
||||||
painter: DotsPainter(),
|
|
||||||
child: const SizedBox(width: double.infinity, height: double.infinity),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Message d'accès restreint
|
// Message d'accès restreint
|
||||||
@@ -245,182 +190,4 @@ class AppScaffold extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper centralisé pour la navigation
|
|
||||||
class NavigationHelper {
|
|
||||||
/// Obtenir la liste des destinations selon le mode d'affichage et le device
|
|
||||||
static List<NavigationDestination> getDestinations({
|
|
||||||
required bool isAdmin,
|
|
||||||
required bool isMobile,
|
|
||||||
}) {
|
|
||||||
final destinations = <NavigationDestination>[];
|
|
||||||
|
|
||||||
// Pages communes à tous les rôles
|
|
||||||
destinations.addAll([
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.dashboard_outlined),
|
|
||||||
selectedIcon: Icon(Icons.dashboard),
|
|
||||||
label: 'Tableau de bord',
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.history_outlined),
|
|
||||||
selectedIcon: Icon(Icons.history),
|
|
||||||
label: 'Historique',
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.map_outlined),
|
|
||||||
selectedIcon: Icon(Icons.map),
|
|
||||||
label: 'Carte',
|
|
||||||
),
|
|
||||||
createBadgedNavigationDestination(
|
|
||||||
icon: const Icon(Icons.chat_outlined),
|
|
||||||
selectedIcon: const Icon(Icons.chat),
|
|
||||||
label: 'Messages',
|
|
||||||
showBadge: true,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Pages spécifiques aux utilisateurs standards
|
|
||||||
if (!isAdmin) {
|
|
||||||
destinations.add(
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.explore_outlined),
|
|
||||||
selectedIcon: Icon(Icons.explore),
|
|
||||||
label: 'Terrain',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pages spécifiques aux admins (seulement sur desktop)
|
|
||||||
if (isAdmin && !isMobile) {
|
|
||||||
destinations.addAll([
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.business_outlined),
|
|
||||||
selectedIcon: Icon(Icons.business),
|
|
||||||
label: 'Amicale & membres',
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.calendar_today_outlined),
|
|
||||||
selectedIcon: Icon(Icons.calendar_today),
|
|
||||||
label: 'Opérations',
|
|
||||||
),
|
|
||||||
const NavigationDestination(
|
|
||||||
icon: Icon(Icons.analytics_outlined),
|
|
||||||
selectedIcon: Icon(Icons.analytics),
|
|
||||||
label: 'Connexions',
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return destinations;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Naviguer vers une page selon l'index et le rôle
|
|
||||||
static void navigateToIndex(BuildContext context, int index, bool isAdmin) {
|
|
||||||
if (isAdmin) {
|
|
||||||
_navigateAdminIndex(context, index);
|
|
||||||
} else {
|
|
||||||
_navigateUserIndex(context, index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigation pour les admins
|
|
||||||
static void _navigateAdminIndex(BuildContext context, int index) {
|
|
||||||
switch (index) {
|
|
||||||
case 0:
|
|
||||||
context.go('/admin');
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
context.go('/admin/history');
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
context.go('/admin/map');
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
context.go('/admin/messages');
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
context.go('/admin/amicale');
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
context.go('/admin/operations');
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
context.go('/admin/connexions');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
context.go('/admin');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Navigation pour les utilisateurs standards
|
|
||||||
static void _navigateUserIndex(BuildContext context, int index) {
|
|
||||||
switch (index) {
|
|
||||||
case 0:
|
|
||||||
context.go('/user/dashboard');
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
context.go('/user/history');
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
context.go('/user/map');
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
context.go('/user/messages');
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
context.go('/user/field-mode');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
context.go('/user/dashboard');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtenir l'index selon la route actuelle et le rôle
|
|
||||||
static int getIndexFromRoute(String route, bool isAdmin) {
|
|
||||||
// Enlever les paramètres de query si présents
|
|
||||||
final cleanRoute = route.split('?').first;
|
|
||||||
|
|
||||||
if (isAdmin) {
|
|
||||||
if (cleanRoute.contains('/admin/history')) return 1;
|
|
||||||
if (cleanRoute.contains('/admin/map')) return 2;
|
|
||||||
if (cleanRoute.contains('/admin/messages')) return 3;
|
|
||||||
if (cleanRoute.contains('/admin/amicale')) return 4;
|
|
||||||
if (cleanRoute.contains('/admin/operations')) return 5;
|
|
||||||
if (cleanRoute.contains('/admin/connexions')) return 6;
|
|
||||||
return 0; // Dashboard par défaut
|
|
||||||
} else {
|
|
||||||
if (cleanRoute.contains('/user/history')) return 1;
|
|
||||||
if (cleanRoute.contains('/user/map')) return 2;
|
|
||||||
if (cleanRoute.contains('/user/messages')) return 3;
|
|
||||||
if (cleanRoute.contains('/user/field-mode')) return 4;
|
|
||||||
return 0; // Dashboard par défaut
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtenir le nom de la page selon l'index et le rôle
|
|
||||||
static String getPageNameFromIndex(int index, bool isAdmin) {
|
|
||||||
if (isAdmin) {
|
|
||||||
switch (index) {
|
|
||||||
case 0: return 'dashboard';
|
|
||||||
case 1: return 'history';
|
|
||||||
case 2: return 'map';
|
|
||||||
case 3: return 'messages';
|
|
||||||
case 4: return 'amicale';
|
|
||||||
case 5: return 'operations';
|
|
||||||
case 6: return 'connexions';
|
|
||||||
default: return 'dashboard';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (index) {
|
|
||||||
case 0: return 'dashboard';
|
|
||||||
case 1: return 'history';
|
|
||||||
case 2: return 'map';
|
|
||||||
case 3: return 'messages';
|
|
||||||
case 4: return 'field-mode';
|
|
||||||
default: return 'dashboard';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
46
app/lib/presentation/widgets/backgrounds/dots_painter.dart
Executable file
46
app/lib/presentation/widgets/backgrounds/dots_painter.dart
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
/// Painter pour dessiner des petits points blancs semi-transparents sur un fond
|
||||||
|
/// Utilisé pour créer un effet visuel subtil sur les fonds dégradés
|
||||||
|
class DotsPainter extends CustomPainter {
|
||||||
|
/// Opacité des points (0.0 à 1.0)
|
||||||
|
final double opacity;
|
||||||
|
|
||||||
|
/// Seed pour le générateur aléatoire (pour consistance du pattern)
|
||||||
|
final int seed;
|
||||||
|
|
||||||
|
/// Densité des points (nombre de pixels par point)
|
||||||
|
/// Plus la valeur est élevée, moins il y a de points
|
||||||
|
final int density;
|
||||||
|
|
||||||
|
const DotsPainter({
|
||||||
|
this.opacity = 0.5,
|
||||||
|
this.seed = 42,
|
||||||
|
this.density = 1500,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()
|
||||||
|
..color = Colors.white.withOpacity(opacity)
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
final random = math.Random(seed);
|
||||||
|
final numberOfDots = (size.width * size.height) ~/ density;
|
||||||
|
|
||||||
|
for (int i = 0; i < numberOfDots; i++) {
|
||||||
|
final x = random.nextDouble() * size.width;
|
||||||
|
final y = random.nextDouble() * size.height;
|
||||||
|
final radius = 1.0 + random.nextDouble() * 2.0;
|
||||||
|
canvas.drawCircle(Offset(x, y), radius, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant DotsPainter oldDelegate) {
|
||||||
|
return oldDelegate.opacity != opacity ||
|
||||||
|
oldDelegate.seed != seed ||
|
||||||
|
oldDelegate.density != density;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
app/lib/presentation/widgets/backgrounds/gradient_background.dart
Executable file
49
app/lib/presentation/widgets/backgrounds/gradient_background.dart
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:geosector_app/presentation/widgets/backgrounds/dots_painter.dart';
|
||||||
|
|
||||||
|
/// Widget de fond dégradé avec points décoratifs
|
||||||
|
/// Utilisé comme fond pour les pages admin et user avec des couleurs différentes
|
||||||
|
class GradientBackground extends StatelessWidget {
|
||||||
|
/// Indique si le fond est pour un admin (rouge) ou user (vert)
|
||||||
|
final bool isAdmin;
|
||||||
|
|
||||||
|
/// Afficher ou non les points décoratifs
|
||||||
|
final bool showDots;
|
||||||
|
|
||||||
|
/// Opacité des points (0.0 à 1.0)
|
||||||
|
final double dotsOpacity;
|
||||||
|
|
||||||
|
const GradientBackground({
|
||||||
|
super.key,
|
||||||
|
required this.isAdmin,
|
||||||
|
this.showDots = true,
|
||||||
|
this.dotsOpacity = 0.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Couleurs de fond selon le rôle
|
||||||
|
final gradientColors = isAdmin
|
||||||
|
? [Colors.white, Colors.red.shade300] // Admin: dégradé rouge
|
||||||
|
: [Colors.white, Colors.green.shade300]; // User: dégradé vert
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: gradientColors,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: showDots
|
||||||
|
? CustomPaint(
|
||||||
|
painter: DotsPainter(opacity: dotsOpacity),
|
||||||
|
child: const SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,92 +48,30 @@ class DashboardLayout extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
try {
|
// Scaffold avec fond transparent (le fond est géré par AppScaffold)
|
||||||
debugPrint('Building DashboardLayout');
|
return Scaffold(
|
||||||
|
key: ValueKey('dashboard_scaffold_$selectedIndex'),
|
||||||
// Vérifier que les destinations ne sont pas vides
|
backgroundColor: Colors.transparent,
|
||||||
if (destinations.isEmpty) {
|
appBar: DashboardAppBar(
|
||||||
debugPrint('ERREUR: destinations est vide dans DashboardLayout');
|
key: ValueKey('dashboard_appbar_$selectedIndex'),
|
||||||
return const Scaffold(
|
title: title,
|
||||||
body: Center(
|
pageTitle: destinations[selectedIndex].label,
|
||||||
child: Text('Erreur: Aucune destination de navigation disponible'),
|
isAdmin: isAdmin,
|
||||||
),
|
onLogoutPressed: onLogoutPressed,
|
||||||
);
|
),
|
||||||
}
|
body: ResponsiveNavigation(
|
||||||
|
key: ValueKey('responsive_nav_$selectedIndex'),
|
||||||
// Vérifier que selectedIndex est valide
|
title: title,
|
||||||
if (selectedIndex < 0 || selectedIndex >= destinations.length) {
|
body: body,
|
||||||
debugPrint('ERREUR: selectedIndex invalide dans DashboardLayout');
|
selectedIndex: selectedIndex,
|
||||||
return Scaffold(
|
onDestinationSelected: onDestinationSelected,
|
||||||
body: Center(
|
destinations: destinations,
|
||||||
child:
|
showNewPassageButton: false,
|
||||||
Text('Erreur: Index de navigation invalide ($selectedIndex)'),
|
onNewPassagePressed: null,
|
||||||
),
|
sidebarBottomItems: sidebarBottomItems,
|
||||||
);
|
isAdmin: isAdmin,
|
||||||
}
|
showAppBar: false,
|
||||||
|
),
|
||||||
// Scaffold avec fond transparent (le fond est géré par AppScaffold)
|
);
|
||||||
return Scaffold(
|
|
||||||
key: ValueKey('dashboard_scaffold_$selectedIndex'),
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
appBar: DashboardAppBar(
|
|
||||||
key: ValueKey('dashboard_appbar_$selectedIndex'),
|
|
||||||
title: title,
|
|
||||||
pageTitle: destinations[selectedIndex].label,
|
|
||||||
isAdmin: isAdmin,
|
|
||||||
onLogoutPressed: onLogoutPressed,
|
|
||||||
),
|
|
||||||
body: ResponsiveNavigation(
|
|
||||||
key: ValueKey('responsive_nav_$selectedIndex'),
|
|
||||||
title:
|
|
||||||
title, // Même si le titre n'est pas affiché dans la navigation, il est utilisé pour la cohérence
|
|
||||||
body: body,
|
|
||||||
selectedIndex: selectedIndex,
|
|
||||||
onDestinationSelected: onDestinationSelected,
|
|
||||||
destinations: destinations,
|
|
||||||
// Ne pas afficher le bouton "Nouveau passage" dans la navigation
|
|
||||||
showNewPassageButton: false,
|
|
||||||
onNewPassagePressed: null,
|
|
||||||
sidebarBottomItems: sidebarBottomItems,
|
|
||||||
isAdmin: isAdmin,
|
|
||||||
// Ne pas afficher l'AppBar dans la navigation car nous utilisons DashboardAppBar
|
|
||||||
showAppBar: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('ERREUR CRITIQUE dans DashboardLayout.build: $e');
|
|
||||||
// Afficher une interface de secours en cas d'erreur
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text('Erreur - $title'),
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
body: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.error_outline, color: Colors.red, size: 64),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'Une erreur est survenue',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: AppTheme.r(context, 20),
|
|
||||||
fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text('Détails: $e'),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context)
|
|
||||||
.pushNamedAndRemoveUntil('/', (route) => false);
|
|
||||||
},
|
|
||||||
child: const Text('Retour à l\'accueil'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user