feat: synchronisation mode deconnecte fin chat et stats
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
286
app/lib/presentation/widgets/offline_test_button.dart
Normal file
286
app/lib/presentation/widgets/offline_test_button.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
269
app/lib/presentation/widgets/pending_requests_counter.dart
Normal file
269
app/lib/presentation/widgets/pending_requests_counter.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user