Files
geo/flutt/lib/presentation/widgets/responsive_navigation.dart

688 lines
22 KiB
Dart

import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:flutter/material.dart';
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/presentation/widgets/help_dialog.dart';
import 'package:geosector_app/presentation/widgets/profile_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({
Key? 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,
}) : super(key: key);
@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(
appBar: widget.showAppBar
? AppBar(
title: Text(widget.title),
actions: _buildAppBarActions(context),
)
: null,
body: isDesktop ? _buildDesktopLayout() : _buildMobileLayout(),
bottomNavigationBar: (isDesktop) ? null : _buildBottomNavigationBar(),
);
}
/// Construction du layout pour les écrans de bureau (web)
Widget _buildDesktopLayout() {
// Utiliser une couleur de fond différente selon le type d'utilisateur
final backgroundColor = widget.isAdmin
? const Color(0xFFFFEBEE) // Fond rouge clair pour l'interface admin
: const Color(
0xFFE8F5E9); // Fond vert clair pour l'interface utilisateur
return Row(
children: [
_buildSidebar(),
Expanded(
child: Container(
color: backgroundColor,
child: widget.body,
),
),
],
);
}
/// Construction du layout pour les écrans mobiles
Widget _buildMobileLayout() {
// Utiliser une couleur de fond différente selon le type d'utilisateur
final backgroundColor = widget.isAdmin
? const Color(0xFFFFEBEE) // Fond rouge clair pour l'interface admin
: const Color(
0xFFE8F5E9); // Fond vert clair pour l'interface utilisateur
return Container(
color: backgroundColor,
child: widget.body,
);
}
/// Construction des actions de l'AppBar
List<Widget> _buildAppBarActions(BuildContext context) {
List<Widget> actions = [];
// Ajouter les actions supplémentaires si elles existent
if (widget.additionalActions != null &&
widget.additionalActions!.isNotEmpty) {
actions.addAll(widget.additionalActions!);
} else if (widget.showNewPassageButton && widget.selectedIndex == 0) {
// Ajouter le bouton "Nouveau passage" en haut à droite pour la page d'accueil
actions.add(
TextButton.icon(
icon: const Icon(Icons.add_location_alt, color: Colors.white),
label: const Text('Nouveau passage',
style: TextStyle(color: Colors.white)),
onPressed: widget.onNewPassagePressed ??
() {
// Fonction par défaut si onNewPassagePressed n'est pas fourni
_showPassageForm(context);
},
style: TextButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.secondary,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
),
);
actions.add(const SizedBox(width: 16)); // Espacement à droite
}
return actions;
}
/// Construction de la barre de navigation inférieure pour mobile
Widget _buildBottomNavigationBar() {
final theme = Theme.of(context);
return NavigationBar(
selectedIndex: widget.selectedIndex,
onDestinationSelected: widget.onDestinationSelected,
backgroundColor: theme.colorScheme.surface,
elevation: 8,
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
if (widget.sidebarBottomItems != null && !_isSidebarMinimized)
...widget.sidebarBottomItems!,
// Éléments par défaut du bas de la sidebar
if (!_isSidebarMinimized)
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'Paramètres',
style: theme.textTheme.titleSmall?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
_SettingsItem(
icon: Icons.person,
title: 'Mon compte',
subtitle: null,
isSidebarMinimized: _isSidebarMinimized,
onTap: () {
// Afficher la boîte de dialogue de profil avec l'ID de l'utilisateur actuel
// Utiliser l'instance globale définie dans app.dart
final user = userRepository.currentUser;
if (user != null && user.id != null) {
// Convertir l'ID en chaîne de caractères si nécessaire
ProfileDialog.show(context, user.id!.toString());
} else {
// Afficher un message d'erreur si l'utilisateur n'est pas trouvé
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Erreur: Utilisateur non trouvé'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
},
),
if (widget.isAdmin && userRepository.currentUser?.role == 2)
_SettingsItem(
icon: Icons.people,
title: 'Amicale & membres',
isSidebarMinimized: _isSidebarMinimized,
onTap: () {
// Navigation vers le tableau de bord admin avec sélection de l'onglet "Amicale et membres"
context.go('/admin');
// Sélectionner l'onglet "Amicale et membres" (index 5)
// Nous devons sauvegarder cet index dans les paramètres pour que le tableau de bord
// puisse le récupérer et sélectionner le bon onglet
final settingsBox = Hive.box(AppKeys.settingsBoxName);
settingsBox.put('adminSelectedPageIndex', 5);
},
),
const SizedBox(height: 16),
_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);
},
),
],
),
),
);
}
/// 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;
final IconData? iconData = (icon is Icon) ? (icon as Icon).icon : null;
// 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
? theme.colorScheme.primary.withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: iconData != null
? Icon(
iconData,
color: isSelected
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withOpacity(0.6),
size: 24,
)
: icon,
),
),
),
);
} else {
// Version normale avec texte et icône
return ListTile(
leading: iconData != null
? Icon(
iconData,
color: isSelected
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withOpacity(0.6),
)
: icon,
title: Text(
displayTitle,
style: TextStyle(
color: isSelected
? theme.colorScheme.primary
: theme.colorScheme.onSurface,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
tileColor:
isSelected ? theme.colorScheme.primary.withOpacity(0.1) : null,
onTap: () {
widget.onDestinationSelected(index);
},
);
}
}
/// Affiche le formulaire de passage
void _showPassageForm(BuildContext context) {
final theme = Theme.of(context);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(
'Nouveau passage',
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
decoration: InputDecoration(
labelText: 'Adresse',
prefixIcon: const Icon(Icons.location_on),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
decoration: InputDecoration(
labelText: 'Type de passage',
prefixIcon: const Icon(Icons.category),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
items: const [
DropdownMenuItem(
value: 1,
child: Text('Effectué'),
),
DropdownMenuItem(
value: 2,
child: Text('À finaliser'),
),
DropdownMenuItem(
value: 3,
child: Text('Refusé'),
),
DropdownMenuItem(
value: 4,
child: Text('Don'),
),
DropdownMenuItem(
value: 5,
child: Text('Lot'),
),
DropdownMenuItem(
value: 6,
child: Text('Maison vide'),
),
],
onChanged: (value) {},
),
const SizedBox(height: 16),
TextField(
decoration: InputDecoration(
labelText: 'Commentaire',
prefixIcon: const Icon(Icons.comment),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
maxLines: 3,
),
],
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
'Annuler',
style: TextStyle(
color: theme.colorScheme.error,
),
),
),
ElevatedButton(
onPressed: () {
// Enregistrer le passage
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Passage enregistré avec succès'),
backgroundColor: theme.colorScheme.primary,
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: theme.colorScheme.onPrimary,
),
child: const Text('Enregistrer'),
),
],
),
);
}
}
/// Widget pour les éléments de paramètres
class _SettingsItem extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final Widget? trailing;
final VoidCallback onTap;
final bool isSidebarMinimized;
const _SettingsItem({
required this.icon,
required this.title,
this.subtitle,
this.trailing,
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),
subtitle: subtitle != null ? Text(subtitle!) : null,
trailing: trailing,
onTap: onTap,
);
}
}
}