feat: synchronisation mode deconnecte fin chat et stats

This commit is contained in:
2025-08-31 18:21:20 +02:00
parent 41a4505b4b
commit 604294af96
149 changed files with 285769 additions and 250633 deletions

View File

@@ -20,11 +20,15 @@ NavigationDestination createBadgedNavigationDestination({
final badgedIcon = BadgedIcon(
icon: icon.icon!,
showBadge: true,
color: icon.color,
size: icon.size,
);
final badgedSelectedIcon = BadgedIcon(
icon: selectedIcon.icon!,
showBadge: true,
color: selectedIcon.color,
size: selectedIcon.size,
);
return NavigationDestination(

View File

@@ -52,6 +52,9 @@ class PaymentPieChart extends StatefulWidget {
/// ID de l'utilisateur pour filtrer les passages
final int? userId;
/// Afficher tous les passages (admin) ou seulement ceux de l'utilisateur
final bool showAllPassages;
const PaymentPieChart({
super.key,
this.payments = const [],
@@ -68,6 +71,7 @@ class PaymentPieChart extends StatefulWidget {
this.useGradient = false,
this.useValueListenable = true,
this.userId,
this.showAllPassages = false,
});
@override
@@ -97,7 +101,8 @@ class _PaymentPieChartState extends State<PaymentPieChart>
bool shouldResetAnimation = false;
if (widget.useValueListenable != oldWidget.useValueListenable ||
widget.userId != oldWidget.userId) {
widget.userId != oldWidget.userId ||
widget.showAllPassages != oldWidget.showAllPassages) {
shouldResetAnimation = true;
} else if (!widget.useValueListenable) {
// Pour les données statiques, comparer les éléments
@@ -158,7 +163,11 @@ class _PaymentPieChartState extends State<PaymentPieChart>
try {
final passages = passagesBox.values.toList();
final currentUser = userRepository.getCurrentUser();
final int? currentUserId = widget.userId ?? currentUser?.id;
// Déterminer l'utilisateur cible selon les filtres
final int? targetUserId = widget.showAllPassages
? null
: (widget.userId ?? currentUser?.id);
// Initialiser les montants par type de règlement
final Map<int, double> paymentAmounts = {
@@ -170,8 +179,13 @@ class _PaymentPieChartState extends State<PaymentPieChart>
// Parcourir les passages et calculer les montants par type de règlement
for (final passage in passages) {
// Vérifier si le passage appartient à l'utilisateur actuel
if (currentUserId != null && passage.fkUser == currentUserId) {
// Appliquer le filtre utilisateur si nécessaire
bool shouldInclude = true;
if (targetUserId != null && passage.fkUser != targetUserId) {
shouldInclude = false;
}
if (shouldInclude) {
final int typeReglement = passage.fkTypeReglement;
// Convertir la chaîne de montant en double

View File

@@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:hive_flutter/hive_flutter.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/pending_request.dart';
/// Widget qui affiche l'état de la connexion Internet
class ConnectivityIndicator extends StatelessWidget {
/// Widget qui affiche l'état de la connexion Internet et le nombre de requêtes en attente
class ConnectivityIndicator extends StatefulWidget {
/// Si true, affiche un message d'erreur lorsque l'appareil est déconnecté
final bool showErrorMessage;
@@ -20,6 +23,52 @@ class ConnectivityIndicator extends StatelessWidget {
this.onConnectivityChanged,
});
@override
State<ConnectivityIndicator> createState() => _ConnectivityIndicatorState();
}
class _ConnectivityIndicatorState extends State<ConnectivityIndicator>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
// Configuration de l'animation de clignotement
_animationController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_animation = Tween<double>(
begin: 1.0,
end: 0.3,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _updateAnimation(int pendingCount) {
if (pendingCount > 0) {
// Démarrer l'animation de clignotement si des requêtes sont en attente
if (!_animationController.isAnimating) {
_animationController.repeat(reverse: true);
}
} else {
// Arrêter l'animation quand il n'y a plus de requêtes
_animationController.stop();
_animationController.value = 1.0;
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@@ -32,12 +81,159 @@ class ConnectivityIndicator extends StatelessWidget {
// Appeler le callback si fourni, mais pas directement dans le build
// pour éviter les problèmes de rendu
WidgetsBinding.instance.addPostFrameCallback((_) {
if (onConnectivityChanged != null) {
onConnectivityChanged!(isConnected);
if (widget.onConnectivityChanged != null) {
widget.onConnectivityChanged!(isConnected);
}
});
if (!isConnected && showErrorMessage) {
// Vérifier si la box des requêtes en attente est ouverte
if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
return _buildBasicIndicator(context, isConnected, connectionType, connectionStatus, theme, 0);
}
// Utiliser ValueListenableBuilder pour surveiller les requêtes en attente
return ValueListenableBuilder<Box<PendingRequest>>(
valueListenable: Hive.box<PendingRequest>(AppKeys.pendingRequestsBoxName).listenable(),
builder: (context, box, child) {
final pendingCount = box.length;
// Mettre à jour l'animation en fonction du nombre de requêtes
_updateAnimation(pendingCount);
if (!isConnected && widget.showErrorMessage) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: theme.colorScheme.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: theme.colorScheme.error.withOpacity(0.3),
),
),
child: Row(
children: [
Icon(
Icons.wifi_off,
color: theme.colorScheme.error,
size: 18,
),
const SizedBox(width: 8),
Expanded(
child: Text(
pendingCount > 0
? 'Hors ligne - $pendingCount requête${pendingCount > 1 ? 's' : ''} en attente'
: 'Aucune connexion Internet. Certaines fonctionnalités peuvent être limitées.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.error,
),
),
),
if (pendingCount > 0)
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Opacity(
opacity: _animation.value,
child: Container(
margin: const EdgeInsets.only(left: 8),
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
),
child: Text(
pendingCount.toString(),
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
);
},
),
],
),
);
} else if (isConnected && widget.showConnectionType) {
return _buildConnectedIndicator(
context,
connectionStatus,
connectionType,
theme,
pendingCount
);
}
// Si aucune condition n'est remplie
return const SizedBox.shrink();
},
);
}
Widget _buildConnectedIndicator(
BuildContext context,
List<ConnectivityResult> connectionStatus,
String connectionType,
ThemeData theme,
int pendingCount,
) {
// Obtenir la couleur et l'icône en fonction du type de connexion
final color = _getConnectionColor(connectionStatus, theme);
final icon = _getConnectionIcon(connectionStatus);
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(
color: pendingCount > 0
? Colors.orange.withOpacity(0.1 * _animation.value)
: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: pendingCount > 0
? Colors.orange.withOpacity(0.3 * _animation.value)
: color.withOpacity(0.3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
pendingCount > 0 ? Icons.sync : icon,
color: pendingCount > 0 ? Colors.orange : color,
size: 14,
),
const SizedBox(width: 4),
Text(
pendingCount > 0
? '$pendingCount en attente'
: connectionType,
style: theme.textTheme.bodySmall?.copyWith(
color: pendingCount > 0 ? Colors.orange : color,
fontWeight: FontWeight.bold,
),
),
],
),
);
},
);
}
Widget _buildBasicIndicator(
BuildContext context,
bool isConnected,
String connectionType,
List<ConnectivityResult> connectionStatus,
ThemeData theme,
int pendingCount,
) {
if (!isConnected && widget.showErrorMessage) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
margin: const EdgeInsets.only(bottom: 8),
@@ -67,8 +263,7 @@ class ConnectivityIndicator extends StatelessWidget {
],
),
);
} else if (isConnected && showConnectionType) {
// Obtenir la couleur et l'icône en fonction du type de connexion
} else if (isConnected && widget.showConnectionType) {
final color = _getConnectionColor(connectionStatus, theme);
final icon = _getConnectionIcon(connectionStatus);
@@ -102,7 +297,6 @@ class ConnectivityIndicator extends StatelessWidget {
);
}
// Si aucune condition n'est remplie ou si showErrorMessage et showConnectionType sont false
return const SizedBox.shrink();
}

View File

@@ -0,0 +1,286 @@
import 'package:flutter/material.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/services/connectivity_service.dart';
import 'package:geosector_app/core/utils/api_exception.dart';
import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:uuid/uuid.dart';
/// Widget de test pour vérifier le fonctionnement de la file d'attente offline
/// À utiliser uniquement en développement
class OfflineTestButton extends StatefulWidget {
const OfflineTestButton({Key? key}) : super(key: key);
@override
State<OfflineTestButton> createState() => _OfflineTestButtonState();
}
class _OfflineTestButtonState extends State<OfflineTestButton> {
final _uuid = const Uuid();
bool _isProcessing = false;
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Test de synchronisation offline',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'Utilisez ces boutons pour tester la mise en file d\'attente des requêtes',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 16),
// Indicateur de connectivité
ListenableBuilder(
listenable: ConnectivityService(),
builder: (context, child) {
final isConnected = ConnectivityService().isConnected;
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isConnected ? Colors.green.shade100 : Colors.red.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
isConnected ? Icons.wifi : Icons.wifi_off,
color: isConnected ? Colors.green : Colors.red,
),
const SizedBox(width: 8),
Text(
isConnected ? 'Connecté' : 'Hors ligne',
style: TextStyle(
color: isConnected ? Colors.green : Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
);
},
),
const SizedBox(height: 16),
// Boutons de test
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: _isProcessing ? null : _testGetRequest,
icon: const Icon(Icons.download),
label: const Text('Test GET'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
),
),
ElevatedButton.icon(
onPressed: _isProcessing ? null : _testPostRequest,
icon: const Icon(Icons.upload),
label: const Text('Test POST'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
),
ElevatedButton.icon(
onPressed: _isProcessing ? null : _testPutRequest,
icon: const Icon(Icons.edit),
label: const Text('Test PUT'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
),
ElevatedButton.icon(
onPressed: _isProcessing ? null : _testDeleteRequest,
icon: const Icon(Icons.delete),
label: const Text('Test DELETE'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
),
ElevatedButton.icon(
onPressed: _isProcessing ? null : _processQueue,
icon: const Icon(Icons.sync),
label: const Text('Traiter la file'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple,
),
),
],
),
if (_isProcessing)
const Padding(
padding: EdgeInsets.only(top: 16),
child: Center(
child: CircularProgressIndicator(),
),
),
],
),
),
);
}
Future<void> _testGetRequest() async {
setState(() => _isProcessing = true);
try {
debugPrint('🧪 Test GET request');
final response = await ApiService.instance.get('/test/endpoint');
if (response.data['queued'] == true) {
if (mounted) {
ApiException.showSuccess(context, 'Requête GET mise en file d\'attente');
}
} else {
if (mounted) {
ApiException.showSuccess(context, 'Requête GET exécutée avec succès');
}
}
} catch (e) {
if (mounted) {
ApiException.showError(context, e);
}
} finally {
setState(() => _isProcessing = false);
}
}
Future<void> _testPostRequest() async {
setState(() => _isProcessing = true);
try {
final tempId = 'temp_${_uuid.v4()}';
debugPrint('🧪 Test POST request avec tempId: $tempId');
final testData = {
'name': 'Test User ${DateTime.now().millisecondsSinceEpoch}',
'email': 'test@example.com',
'timestamp': DateTime.now().toIso8601String(),
};
final response = await ApiService.instance.post(
'/test/create',
data: testData,
tempId: tempId,
);
if (response.data['queued'] == true) {
if (mounted) {
ApiException.showSuccess(context, 'Requête POST mise en file d\'attente (tempId: $tempId)');
}
} else {
if (mounted) {
ApiException.showSuccess(context, 'Requête POST exécutée avec succès');
}
}
} catch (e) {
if (mounted) {
ApiException.showError(context, e);
}
} finally {
setState(() => _isProcessing = false);
}
}
Future<void> _testPutRequest() async {
setState(() => _isProcessing = true);
try {
final tempId = 'temp_${_uuid.v4()}';
debugPrint('🧪 Test PUT request avec tempId: $tempId');
final testData = {
'id': 123,
'name': 'Updated User ${DateTime.now().millisecondsSinceEpoch}',
'email': 'updated@example.com',
'timestamp': DateTime.now().toIso8601String(),
};
final response = await ApiService.instance.put(
'/test/update/123',
data: testData,
tempId: tempId,
);
if (response.data['queued'] == true) {
if (mounted) {
ApiException.showSuccess(context, 'Requête PUT mise en file d\'attente (tempId: $tempId)');
}
} else {
if (mounted) {
ApiException.showSuccess(context, 'Requête PUT exécutée avec succès');
}
}
} catch (e) {
if (mounted) {
ApiException.showError(context, e);
}
} finally {
setState(() => _isProcessing = false);
}
}
Future<void> _testDeleteRequest() async {
setState(() => _isProcessing = true);
try {
final tempId = 'temp_${_uuid.v4()}';
debugPrint('🧪 Test DELETE request avec tempId: $tempId');
final response = await ApiService.instance.delete(
'/test/delete/123',
tempId: tempId,
);
if (response.data['queued'] == true) {
if (mounted) {
ApiException.showSuccess(context, 'Requête DELETE mise en file d\'attente (tempId: $tempId)');
}
} else {
if (mounted) {
ApiException.showSuccess(context, 'Requête DELETE exécutée avec succès');
}
}
} catch (e) {
if (mounted) {
ApiException.showError(context, e);
}
} finally {
setState(() => _isProcessing = false);
}
}
Future<void> _processQueue() async {
setState(() => _isProcessing = true);
try {
debugPrint('🧪 Traitement manuel de la file d\'attente');
await ApiService.instance.processPendingRequests();
if (mounted) {
ApiException.showSuccess(context, 'File d\'attente traitée');
}
} catch (e) {
if (mounted) {
ApiException.showError(context, e);
}
} finally {
setState(() => _isProcessing = false);
}
}
}

View File

@@ -0,0 +1,269 @@
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/core/data/models/pending_request.dart';
/// Widget qui affiche le nombre de requêtes en attente de synchronisation
/// S'affiche uniquement quand il y a au moins une requête en attente
/// Se met à jour automatiquement grâce au ValueListenableBuilder
class PendingRequestsCounter extends StatelessWidget {
final bool showDetails;
final Color? backgroundColor;
final Color? textColor;
const PendingRequestsCounter({
Key? key,
this.showDetails = false,
this.backgroundColor,
this.textColor,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// Vérifier si la box est ouverte
if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
return const SizedBox.shrink();
}
return ValueListenableBuilder<Box<PendingRequest>>(
valueListenable: Hive.box<PendingRequest>(AppKeys.pendingRequestsBoxName).listenable(),
builder: (context, box, child) {
final count = box.length;
// Ne rien afficher s'il n'y a pas de requêtes en attente
if (count == 0) {
return const SizedBox.shrink();
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: backgroundColor ?? Colors.orange.shade100,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.orange.shade300,
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.sync,
size: 16,
color: textColor ?? Colors.orange.shade700,
),
const SizedBox(width: 6),
Text(
count == 1
? '1 requête en attente'
: '$count requêtes en attente',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: textColor ?? Colors.orange.shade700,
),
),
if (showDetails) ...[
const SizedBox(width: 6),
InkWell(
onTap: () => _showPendingRequestsDialog(context, box),
child: Icon(
Icons.info_outline,
size: 16,
color: textColor ?? Colors.orange.shade700,
),
),
],
],
),
);
},
);
}
void _showPendingRequestsDialog(BuildContext context, Box<PendingRequest> box) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Requêtes en attente'),
content: SizedBox(
width: double.maxFinite,
child: ListView.builder(
shrinkWrap: true,
itemCount: box.length,
itemBuilder: (context, index) {
final request = box.getAt(index);
if (request == null) return const SizedBox.shrink();
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: _getMethodColor(request.method),
borderRadius: BorderRadius.circular(4),
),
child: Text(
request.method,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
request.path,
style: const TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 4),
Text(
'Créée: ${_formatDateTime(request.createdAt)}',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
),
),
if (request.retryCount > 0) ...[
const SizedBox(height: 2),
Text(
'Tentatives: ${request.retryCount}',
style: TextStyle(
fontSize: 11,
color: Colors.orange.shade700,
),
),
],
if (request.errorMessage != null) ...[
const SizedBox(height: 2),
Text(
'Erreur: ${request.errorMessage}',
style: const TextStyle(
fontSize: 11,
color: Colors.red,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
],
),
);
}
Color _getMethodColor(String method) {
switch (method.toUpperCase()) {
case 'GET':
return Colors.blue;
case 'POST':
return Colors.green;
case 'PUT':
return Colors.orange;
case 'DELETE':
return Colors.red;
default:
return Colors.grey;
}
}
String _formatDateTime(DateTime dateTime) {
final now = DateTime.now();
final difference = now.difference(dateTime);
if (difference.inSeconds < 60) {
return 'Il y a ${difference.inSeconds}s';
} else if (difference.inMinutes < 60) {
return 'Il y a ${difference.inMinutes}min';
} else if (difference.inHours < 24) {
return 'Il y a ${difference.inHours}h';
} else {
return 'Il y a ${difference.inDays}j';
}
}
}
/// Version compacte du compteur pour les barres d'outils
class PendingRequestsCounterCompact extends StatelessWidget {
final Color? color;
const PendingRequestsCounterCompact({
Key? key,
this.color,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// Vérifier si la box est ouverte
if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
return const SizedBox.shrink();
}
return ValueListenableBuilder<Box<PendingRequest>>(
valueListenable: Hive.box<PendingRequest>(AppKeys.pendingRequestsBoxName).listenable(),
builder: (context, box, child) {
final count = box.length;
// Ne rien afficher s'il n'y a pas de requêtes en attente
if (count == 0) {
return const SizedBox.shrink();
}
return Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: color ?? Colors.orange,
shape: BoxShape.circle,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.sync,
size: 14,
color: Colors.white,
),
const SizedBox(width: 4),
Text(
count.toString(),
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
);
},
);
}
}

View File

@@ -356,11 +356,25 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
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).icon : null;
// 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.withOpacity(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;
@@ -391,13 +405,7 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: iconData != null
? Icon(
iconData,
color: isSelected ? selectedColor : unselectedColor,
size: 24,
)
: icon,
child: Center(child: iconWidget),
),
),
),
@@ -405,12 +413,7 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
} else {
// Version normale avec texte et icône
return ListTile(
leading: iconData != null
? Icon(
iconData,
color: isSelected ? selectedColor : unselectedColor,
)
: icon,
leading: iconWidget,
title: Text(
displayTitle,
style: TextStyle(
@@ -432,8 +435,6 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
class _SettingsItem extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final Widget? trailing;
final VoidCallback onTap;
final bool isSidebarMinimized;
@@ -442,8 +443,6 @@ class _SettingsItem extends StatelessWidget {
required this.title,
required this.onTap,
required this.isSidebarMinimized,
this.subtitle,
this.trailing,
});
@override
@@ -482,8 +481,6 @@ class _SettingsItem extends StatelessWidget {
color: theme.colorScheme.primary,
),
title: Text(title),
subtitle: subtitle != null ? Text(subtitle!) : null,
trailing: trailing,
onTap: onTap,
);
}

View File

@@ -53,7 +53,6 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
Widget _buildSortButton(String label, SortType sortType) {
final isActive = _currentSortType == sortType && _currentSortOrder != SortOrder.none;
final isAsc = _currentSortType == sortType && _currentSortOrder == SortOrder.asc;
final isDesc = _currentSortType == sortType && _currentSortOrder == SortOrder.desc;
return InkWell(
onTap: () => _onSortPressed(sortType),
@@ -320,8 +319,8 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
onTap: () {
// Sauvegarder le secteur sélectionné et l'index de la page carte dans Hive
final settingsBox = Hive.box(AppKeys.settingsBoxName);
settingsBox.put('admin_selectedSectorId', sectorId);
settingsBox.put('adminSelectedPageIndex', 4); // Index de la page carte
settingsBox.put('selectedSectorId', sectorId);
settingsBox.put('selectedPageIndex', 4); // Index de la page carte
// Naviguer vers le dashboard admin qui chargera la page carte
context.go('/admin');
@@ -426,7 +425,7 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
settingsBox.put('history_selectedSectorId', sectorId);
settingsBox.put('history_selectedSectorName', sectorName);
settingsBox.put('history_selectedTypeId', typeId);
settingsBox.put('adminSelectedPageIndex', 2); // Index de la page historique
settingsBox.put('selectedPageIndex', 2); // Index de la page historique
// Naviguer vers le dashboard admin qui chargera la page historique
context.go('/admin');

View File

@@ -147,7 +147,6 @@ class ThemeSwitcher extends StatelessWidget {
/// Boutons à bascule
Widget _buildToggleButtons(BuildContext context) {
final themeService = ThemeService.instance;
final theme = Theme.of(context);
return ToggleButtons(
borderRadius: BorderRadius.circular(8),

View File

@@ -989,7 +989,6 @@ class _UserFormState extends State<UserForm> {
required Function(int?)? onChanged,
}) {
final theme = Theme.of(context);
final isSelected = value == groupValue;
return Row(
children: [