Files
geo/app/lib/presentation/widgets/responsive_navigation.dart
Pierre 43d4cd66e1 feat: Mise à jour des interfaces mobiles v3.2.3
- Amélioration des interfaces utilisateur sur mobile
- Optimisation de la responsivité des composants Flutter
- Mise à jour des widgets de chat et communication
- Amélioration des formulaires et tableaux
- Ajout de nouveaux composants pour l'administration
- Optimisation des thèmes et styles visuels

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-02 20:35:40 +02:00

489 lines
15 KiB
Dart
Executable File

import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/presentation/widgets/help_dialog.dart';
/// Widget qui fournit une navigation responsive pour l'application.
/// Affiche une sidebar en mode desktop et une bottomBar en mode mobile.
class ResponsiveNavigation extends StatefulWidget {
/// Le contenu principal à afficher
final Widget body;
/// Le titre de la page
final String title;
/// L'index de la page sélectionnée
final int selectedIndex;
/// Callback appelé lorsqu'un élément de navigation est sélectionné
final Function(int) onDestinationSelected;
/// Liste des destinations de navigation
final List<NavigationDestination> destinations;
/// Actions supplémentaires à afficher dans l'AppBar
final List<Widget>? additionalActions;
/// Indique si le bouton "Nouveau passage" doit être affiché
final bool showNewPassageButton;
/// Callback appelé lorsque le bouton "Nouveau passage" est pressé
final VoidCallback? onNewPassagePressed;
/// Clé de la boîte Hive pour sauvegarder les paramètres
final String settingsBoxKey;
/// Clé pour sauvegarder l'état de la sidebar
final String sidebarStateKey;
/// Widgets à afficher en bas de la sidebar
final List<Widget>? sidebarBottomItems;
/// Indique si l'utilisateur est un administrateur
final bool isAdmin;
/// Indique si l'AppBar doit être affiché
final bool showAppBar;
const ResponsiveNavigation({
super.key,
required this.body,
required this.title,
required this.selectedIndex,
required this.onDestinationSelected,
required this.destinations,
this.additionalActions,
this.showNewPassageButton = true,
this.onNewPassagePressed,
this.settingsBoxKey = AppKeys.settingsBoxName,
this.sidebarStateKey = 'isSidebarMinimized',
this.sidebarBottomItems,
this.isAdmin = false,
this.showAppBar = true,
});
@override
State<ResponsiveNavigation> createState() => _ResponsiveNavigationState();
}
class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
/// État de la barre latérale (minimisée ou non)
bool _isSidebarMinimized = false;
/// Référence à la boîte Hive pour les paramètres
late Box _settingsBox;
@override
void initState() {
super.initState();
_initSettings();
}
/// Initialiser la boîte de paramètres et charger les préférences
Future<void> _initSettings() async {
try {
// Ouvrir la boîte de paramètres si elle n'est pas déjà ouverte
if (!Hive.isBoxOpen(widget.settingsBoxKey)) {
_settingsBox = await Hive.openBox(widget.settingsBoxKey);
} else {
_settingsBox = Hive.box(widget.settingsBoxKey);
}
// Charger l'état de la barre latérale
final sidebarState = _settingsBox.get(widget.sidebarStateKey);
if (sidebarState != null && sidebarState is bool) {
setState(() {
_isSidebarMinimized = sidebarState;
});
}
} catch (e) {
debugPrint('Erreur lors du chargement des paramètres: $e');
}
}
/// Sauvegarder l'état de la barre latérale
void _saveSettings() {
try {
// Sauvegarder l'état de la barre latérale
_settingsBox.put(widget.sidebarStateKey, _isSidebarMinimized);
} catch (e) {
debugPrint('Erreur lors de la sauvegarde des paramètres: $e');
}
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final isDesktop = size.width > 900;
return Scaffold(
backgroundColor: Colors.transparent, // Fond transparent pour voir le gradient
appBar: widget.showAppBar
? AppBar(
title: Text(widget.title),
)
: null,
body: isDesktop ? _buildDesktopLayout() : _buildMobileLayout(),
bottomNavigationBar: (isDesktop) ? null : _buildBottomNavigationBar(),
);
}
/// Construction du layout pour les écrans de bureau (web)
Widget _buildDesktopLayout() {
return Row(
children: [
_buildSidebar(),
Expanded(
child: Container(
color: Colors
.transparent, // Fond transparent pour voir l'AdminBackground
child: widget.body,
),
),
],
);
}
/// Construction du layout pour les écrans mobiles
Widget _buildMobileLayout() {
return Container(
color: Colors.transparent, // Fond transparent pour voir l'AdminBackground
child: widget.body,
);
}
/// Construction de la barre de navigation inférieure pour mobile
Widget _buildBottomNavigationBar() {
final theme = Theme.of(context);
// Définir les couleurs selon le rôle (admin = rouge, user = vert)
final Color selectedColor = widget.isAdmin ? Colors.red : Colors.green;
return Theme(
data: theme.copyWith(
colorScheme: theme.colorScheme.copyWith(
onSecondaryContainer: selectedColor, // Couleur de l'icône sélectionnée
secondaryContainer: selectedColor.withValues(alpha: 0.15), // Couleur de fond de l'indicateur
),
),
child: NavigationBar(
selectedIndex: widget.selectedIndex,
onDestinationSelected: widget.onDestinationSelected,
backgroundColor: theme.colorScheme.surface,
elevation: 8,
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
destinations: widget.destinations,
),
);
}
/// Obtenir le nom complet de l'utilisateur (prénom + nom)
String _getFullUserName(BuildContext context) {
// Utiliser l'instance globale définie dans app.dart
final user = userRepository.currentUser;
if (user == null) return 'Utilisateur';
String fullName = '';
// Ajouter le prénom si disponible
if (user.firstName != null && user.firstName!.isNotEmpty) {
fullName += user.firstName!;
}
// Ajouter le nom
if (user.name != null && user.name!.isNotEmpty) {
// Ajouter un espace si le prénom est déjà présent
if (fullName.isNotEmpty) {
fullName += ' ';
}
fullName += user.name!;
}
// Si aucun nom n'a été trouvé, utiliser 'Utilisateur' par défaut
return fullName.isEmpty ? 'Utilisateur' : fullName;
}
/// Obtenir les initiales du prénom et du nom de l'utilisateur
String _getUserInitials(BuildContext context) {
// Utiliser l'instance globale définie dans app.dart
final user = userRepository.currentUser;
if (user == null) return 'U';
String initials = '';
// Ajouter l'initiale du prénom si disponible
if (user.firstName != null && user.firstName!.isNotEmpty) {
initials += user.firstName!.substring(0, 1).toUpperCase();
}
// Ajouter l'initiale du nom
if (user.name != null && user.name!.isNotEmpty) {
initials += user.name!.substring(0, 1).toUpperCase();
}
// Si aucune initiale n'a été trouvée, utiliser 'U' par défaut
return initials.isEmpty ? 'U' : initials;
}
/// Afficher le sectName entre parenthèses s'il existe
Widget _buildSectNameText(BuildContext context) {
final theme = Theme.of(context);
// Utiliser l'instance globale définie dans app.dart
final user = userRepository.currentUser;
// Si l'utilisateur n'a pas de sectName ou s'il est vide, retourner un widget vide
if (user == null || user.sectName == null || user.sectName!.isEmpty) {
return const SizedBox.shrink();
}
// Sinon, afficher le sectName entre parenthèses
return Text(
'(${user.sectName})',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
);
}
/// Construction de la barre latérale pour la version web
Widget _buildSidebar() {
final theme = Theme.of(context);
return Card(
margin: EdgeInsets.zero,
elevation: 4,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
child: Container(
width: _isSidebarMinimized ? 70 : 250,
color: theme.colorScheme.surface,
child: Column(
children: [
// Bouton pour minimiser/maximiser la barre latérale
Align(
alignment: _isSidebarMinimized
? Alignment.center
: Alignment.centerRight,
child: Padding(
padding:
EdgeInsets.only(top: 8, right: _isSidebarMinimized ? 0 : 8),
child: IconButton(
icon: Icon(_isSidebarMinimized
? Icons.chevron_right
: Icons.chevron_left),
onPressed: () {
setState(() {
_isSidebarMinimized = !_isSidebarMinimized;
_saveSettings(); // Sauvegarder l'état de la barre latérale
});
},
tooltip: _isSidebarMinimized ? 'Développer' : 'Réduire',
),
),
),
const SizedBox(height: 8),
if (!_isSidebarMinimized)
CircleAvatar(
radius: 40,
backgroundColor: theme.colorScheme.primary,
child: Text(
_getUserInitials(context),
style: TextStyle(
fontSize: 28,
color: theme.colorScheme.onPrimary,
),
),
),
const SizedBox(height: 8),
if (!_isSidebarMinimized) ...[
Text(
_getFullUserName(context),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
// Afficher le sectName entre parenthèses s'il existe
_buildSectNameText(context),
Text(
userRepository.currentUser?.email ?? '',
style: theme.textTheme.bodySmall,
),
const SizedBox(height: 24),
] else
const SizedBox(height: 8),
const Divider(),
// Éléments de navigation
for (int i = 0; i < widget.destinations.length; i++)
_buildNavItem(
i, widget.destinations[i].label, widget.destinations[i].icon),
const Spacer(),
const Divider(),
// Éléments du bas de la sidebar (widgets personnalisés)
if (widget.sidebarBottomItems != null && !_isSidebarMinimized)
...widget.sidebarBottomItems!,
// Options administrateur - uniquement pour les administrateurs et en version web
if (widget.isAdmin && MediaQuery.of(context).size.width > 900)
...[],
// Option "Aide" - toujours visible en bas
_SettingsItem(
icon: Icons.help_outline,
title: 'Aide',
isSidebarMinimized: _isSidebarMinimized,
onTap: () {
// Afficher la boîte de dialogue d'aide avec le titre de la page courante
HelpDialog.show(context, widget.title);
},
),
const SizedBox(height: 16),
],
),
),
);
}
/// Construction d'un élément de navigation pour la barre latérale
Widget _buildNavItem(int index, String title, Widget icon) {
final theme = Theme.of(context);
final isSelected = widget.selectedIndex == index;
// Définir les couleurs selon le rôle (admin = rouge, user = vert)
final Color selectedColor = widget.isAdmin ? Colors.red : Colors.green;
final Color unselectedColor = theme.colorScheme.onSurface.withValues(alpha: 0.6);
// Gérer le cas où l'icône est un BadgedIcon ou autre widget composite
Widget iconWidget;
if (icon is Icon) {
// Si c'est une Icon simple, on peut appliquer les couleurs
iconWidget = Icon(
icon.icon,
color: isSelected ? selectedColor : unselectedColor,
size: 24,
);
} else {
// Si c'est un BadgedIcon ou autre widget, on le garde tel quel
// Le BadgedIcon gère ses propres couleurs
iconWidget = icon;
}
// Remplacer certains titres si l'interface est de type "user"
String displayTitle = title;
if (!widget.isAdmin) {
if (title == "Accueil") {
displayTitle = "Tableau de bord";
} else if (title == "Stats") {
displayTitle = "Statistiques";
}
}
if (_isSidebarMinimized) {
// Version minimisée - afficher uniquement l'icône
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Tooltip(
message: displayTitle,
child: InkWell(
onTap: () {
widget.onDestinationSelected(index);
},
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: isSelected
? selectedColor.withValues(alpha: 0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Center(child: iconWidget),
),
),
),
);
} else {
// Version normale avec texte et icône
return ListTile(
leading: iconWidget,
title: Text(
displayTitle,
style: TextStyle(
color: isSelected ? selectedColor : theme.colorScheme.onSurface,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
tileColor:
isSelected ? selectedColor.withValues(alpha: 0.1) : null,
onTap: () {
widget.onDestinationSelected(index);
},
);
}
}
}
/// Widget pour les éléments de paramètres
class _SettingsItem extends StatelessWidget {
final IconData icon;
final String title;
final VoidCallback onTap;
final bool isSidebarMinimized;
const _SettingsItem({
required this.icon,
required this.title,
required this.onTap,
required this.isSidebarMinimized,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (isSidebarMinimized) {
// Version minimisée - afficher uniquement l'icône
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Tooltip(
message: title,
child: InkWell(
onTap: onTap,
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: theme.colorScheme.primary,
size: 24,
),
),
),
),
);
} else {
// Version normale avec texte et icône
return ListTile(
leading: Icon(
icon,
color: theme.colorScheme.primary,
),
title: Text(title),
onTap: onTap,
);
}
}
}