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>
This commit is contained in:
2025-09-02 20:35:40 +02:00
parent 08f4bff358
commit 43d4cd66e1
2133 changed files with 237004 additions and 173303 deletions

View File

@@ -138,14 +138,14 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
if (context.mounted) {
Navigator.of(context).pop();
}
if (mounted) {
if (context.mounted) {
ApiException.showSuccess(context,
'Membre ${updatedMembre.firstName} ${updatedMembre.name} mis à jour');
}
}
} catch (e) {
debugPrint('❌ Erreur mise à jour membre: $e');
if (mounted) {
if (context.mounted) {
ApiException.showError(context, e);
}
}
@@ -348,9 +348,9 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
color: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.withOpacity(0.3)),
border: Border.all(color: Colors.blue.withValues(alpha: 0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -373,7 +373,7 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
),
const SizedBox(height: 8),
DropdownButtonFormField<int>(
value: selectedMemberForTransfer,
initialValue: selectedMemberForTransfer,
decoration: const InputDecoration(
labelText: 'Membre destinataire',
border: OutlineInputBorder(),
@@ -401,7 +401,7 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
color: Colors.green.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: Row(
@@ -429,10 +429,10 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
color: Colors.green.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border:
Border.all(color: Colors.green.withOpacity(0.3)),
Border.all(color: Colors.green.withValues(alpha: 0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -652,18 +652,18 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
}
// Afficher le message de succès avec les informations du membre créé
if (mounted) {
if (context.mounted) {
ApiException.showSuccess(context,
'Membre ${createdMembre.firstName} ${createdMembre.name} ajouté avec succès (ID: ${createdMembre.id})');
}
} else if (mounted) {
} else if (context.mounted) {
// En cas d'échec, ne pas fermer le dialog pour permettre la correction
ApiException.showError(
context, Exception('Erreur lors de la création du membre'));
}
} catch (e) {
debugPrint('❌ Erreur création membre: $e');
if (mounted) {
if (context.mounted) {
// En cas d'exception, ne pas fermer le dialog
ApiException.showError(context, e);
}
@@ -701,9 +701,9 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
padding: const EdgeInsets.all(12),
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
color: Colors.red.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.withOpacity(0.3)),
border: Border.all(color: Colors.red.withValues(alpha: 0.3)),
),
child: Row(
children: [
@@ -752,7 +752,7 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
Icon(
Icons.business_outlined,
size: 64,
color: theme.colorScheme.primary.withOpacity(0.7),
color: theme.colorScheme.primary.withValues(alpha: 0.7),
),
const SizedBox(height: 16),
Text(
@@ -801,7 +801,7 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
@@ -852,7 +852,7 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),

View File

@@ -12,7 +12,7 @@ class DotsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.5)
..color = Colors.white.withValues(alpha: 0.5)
..style = PaintingStyle.fill;
final random = math.Random(42); // Seed fixe pour consistance
@@ -220,31 +220,12 @@ class _AdminDashboardHomePageState extends State<AdminDashboardHomePage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre avec bouton de rafraîchissement sur la même ligne
Row(
children: [
Expanded(
child: Text(
title,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
// Titre
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
// Bouton de rafraîchissement
if (!isLoading)
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Rafraîchir les données',
onPressed: _loadDashboardData,
)
else
const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
),
const SizedBox(height: AppTheme.spacingM),
// Afficher un indicateur de chargement si les données ne sont pas encore chargées

View File

@@ -21,7 +21,7 @@ class DotsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.5)
..color = Colors.white.withValues(alpha: 0.5)
..style = PaintingStyle.fill;
final random = math.Random(42); // Seed fixe pour consistance

View File

@@ -36,7 +36,7 @@ class AdminDebugInfoWidget extends StatelessWidget {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
tileColor: Colors.grey.withOpacity(0.1),
tileColor: Colors.grey.withValues(alpha: 0.1),
),
// Autres options de débogage peuvent être ajoutées ici
],

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -445,7 +445,7 @@ class _AdminOperationsPageState extends State<AdminOperationsPage> {
bottomRight: Radius.circular(8),
),
border: Border.all(
color: theme.colorScheme.primary.withOpacity(0.1),
color: theme.colorScheme.primary.withValues(alpha: 0.1),
width: 1,
),
),
@@ -471,10 +471,10 @@ class _AdminOperationsPageState extends State<AdminOperationsPage> {
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.1),
color: theme.colorScheme.primary.withValues(alpha: 0.1),
border: Border(
bottom: BorderSide(
color: theme.dividerColor.withOpacity(0.3),
color: theme.dividerColor.withValues(alpha: 0.3),
width: 1,
),
),
@@ -544,13 +544,13 @@ class _AdminOperationsPageState extends State<AdminOperationsPage> {
return InkWell(
onTap: operation.isActive ? () => _showEditOperationDialog(operation) : null,
hoverColor: operation.isActive ? theme.colorScheme.primary.withOpacity(0.05) : null,
hoverColor: operation.isActive ? theme.colorScheme.primary.withValues(alpha: 0.05) : null,
child: Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
bottom: BorderSide(
color: theme.dividerColor.withOpacity(0.3),
color: theme.dividerColor.withValues(alpha: 0.3),
width: 1,
),
),
@@ -582,7 +582,7 @@ class _AdminOperationsPageState extends State<AdminOperationsPage> {
Icon(
Icons.edit_outlined,
size: 16,
color: theme.colorScheme.primary.withOpacity(0.6),
color: theme.colorScheme.primary.withValues(alpha: 0.6),
),
const SizedBox(width: 4),
],
@@ -768,7 +768,7 @@ class _AdminOperationsPageState extends State<AdminOperationsPage> {
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
@@ -783,7 +783,7 @@ class _AdminOperationsPageState extends State<AdminOperationsPage> {
Icon(
Icons.calendar_today_outlined,
size: 64,
color: theme.colorScheme.primary.withOpacity(0.5),
color: theme.colorScheme.primary.withValues(alpha: 0.5),
),
const SizedBox(height: 16),
Text(
@@ -796,7 +796,7 @@ class _AdminOperationsPageState extends State<AdminOperationsPage> {
Text(
"Cliquez sur 'Nouvelle opération' pour commencer",
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
],

View File

@@ -13,7 +13,7 @@ class DotsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.5)
..color = Colors.white.withValues(alpha: 0.5)
..style = PaintingStyle.fill;
final random = math.Random(42); // Seed fixe pour consistance
@@ -178,22 +178,6 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Titre et description
Text(
'Analyse des statistiques',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: AppTheme.spacingS),
Text(
'Visualisez les statistiques de passages et de collecte pour votre amicale.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
const SizedBox(height: AppTheme.spacingL),
// Filtres
Card(
elevation: 2,
@@ -598,31 +582,8 @@ class _AdminStatisticsPageState extends State<AdminStatisticsPage> {
}
// Méthode pour obtenir tous les IDs des membres d'un secteur
List<int> _getMemberIdsForSector(int sectorId) {
return _userSectors
.where((us) => us.fkSector == sectorId)
.map((us) => us.id)
.toList();
}
// Méthode pour déterminer quel userId utiliser pour les graphiques
int? _getUserIdForCharts() {
// Si un membre spécifique est sélectionné, utiliser son ID
if (_selectedMember != 'Tous') {
return _getMemberIdFromName(_selectedMember);
}
// Si un secteur est sélectionné mais pas de membre spécifique
// Les widgets actuels ne supportent pas plusieurs userIds
// Donc on ne peut pas filtrer par secteur pour le moment
// TODO: Implémenter le support multi-users ou sectorId dans les widgets
return null; // Afficher tous les passages
}
// Méthode pour déterminer si on doit afficher tous les passages
bool _shouldShowAllPassages() {
// Afficher tous les passages seulement si aucun filtre n'est appliqué
return _selectedMember == 'Tous' && _selectedSector == 'Tous';
}
}

View File

@@ -30,7 +30,7 @@ class DotsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.5)
..color = Colors.white.withValues(alpha: 0.5)
..style = PaintingStyle.fill;
final random = math.Random(42); // Seed fixe pour consistance
@@ -331,7 +331,6 @@ class _LoginPageState extends State<LoginPage> {
// Utiliser l'instance globale de userRepository
final theme = Theme.of(context);
final size = MediaQuery.of(context).size;
// Les permissions sont maintenant gérées dans splash_page
// On n'a plus besoin de ces vérifications ici
@@ -432,8 +431,8 @@ class _LoginPageState extends State<LoginPage> {
child: Card(
elevation: 8,
shadowColor: _loginType == 'user'
? Colors.green.withOpacity(0.5)
: Colors.red.withOpacity(0.5),
? Colors.green.withValues(alpha: 0.5)
: Colors.red.withValues(alpha: 0.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0)),
child: Padding(
@@ -474,7 +473,7 @@ class _LoginPageState extends State<LoginPage> {
'Bienvenue sur GEOSECTOR',
style: theme.textTheme.bodyLarge?.copyWith(
color:
theme.colorScheme.onSurface.withOpacity(0.7),
theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
@@ -489,11 +488,11 @@ class _LoginPageState extends State<LoginPage> {
margin: const EdgeInsets.only(top: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.error.withOpacity(0.1),
color: theme.colorScheme.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color:
theme.colorScheme.error.withOpacity(0.3),
theme.colorScheme.error.withValues(alpha: 0.3),
),
),
child: Column(
@@ -729,6 +728,7 @@ class _LoginPageState extends State<LoginPage> {
'Login: Tentative avec type: $_loginType');
// Utiliser le nouveau spinner moderne pour la connexion
if (!mounted) return;
final success = await userRepository
.loginWithSpinner(
context,
@@ -888,17 +888,17 @@ class _LoginPageState extends State<LoginPage> {
vertical: 6,
),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.1),
color: theme.colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: theme.colorScheme.primary.withOpacity(0.3),
color: theme.colorScheme.primary.withValues(alpha: 0.3),
width: 1,
),
),
child: Text(
'v$_appVersion',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.primary.withOpacity(0.8),
color: theme.colorScheme.primary.withValues(alpha: 0.8),
fontSize: 12,
fontWeight: FontWeight.w500,
),

View File

@@ -28,7 +28,7 @@ class DotsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.5)
..color = Colors.white.withValues(alpha: 0.5)
..style = PaintingStyle.fill;
final random = math.Random(42); // Seed fixe pour consistance
@@ -279,7 +279,6 @@ class _RegisterPageState extends State<RegisterPage> {
Widget build(BuildContext context) {
// Utiliser l'instance globale de userRepository définie dans app.dart
final theme = Theme.of(context);
final size = MediaQuery.of(context).size;
return Scaffold(
body: Stack(
@@ -328,7 +327,7 @@ class _RegisterPageState extends State<RegisterPage> {
Text(
'Enregistrez votre amicale sur GeoSector',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
@@ -353,10 +352,10 @@ class _RegisterPageState extends State<RegisterPage> {
margin: const EdgeInsets.only(top: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.error.withOpacity(0.1),
color: theme.colorScheme.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: theme.colorScheme.error.withOpacity(0.3),
color: theme.colorScheme.error.withValues(alpha: 0.3),
),
),
child: Column(
@@ -385,7 +384,7 @@ class _RegisterPageState extends State<RegisterPage> {
ElevatedButton.icon(
onPressed: () async {
await _checkConnectivity();
if (_isConnected && mounted) {
if (_isConnected && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
@@ -521,7 +520,7 @@ class _RegisterPageState extends State<RegisterPage> {
color: const Color(0xFFECEFF1),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
@@ -536,7 +535,7 @@ class _RegisterPageState extends State<RegisterPage> {
),
)
: DropdownButtonFormField<City>(
value: _selectedCity,
initialValue: _selectedCity,
decoration: InputDecoration(
prefixIcon: Icon(
Icons.location_city_outlined,
@@ -668,7 +667,7 @@ class _RegisterPageState extends State<RegisterPage> {
.checkConnectivity();
if (!connectivityService.isConnected) {
if (mounted) {
if (context.mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
@@ -685,7 +684,7 @@ class _RegisterPageState extends State<RegisterPage> {
.checkConnectivity();
if (connectivityService
.isConnected &&
mounted) {
context.mounted) {
ScaffoldMessenger.of(
context)
.showSnackBar(
@@ -709,6 +708,7 @@ class _RegisterPageState extends State<RegisterPage> {
_captchaController.text);
if (captchaAnswer !=
_captchaNum1 + _captchaNum2) {
if (!context.mounted) return;
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
@@ -783,7 +783,7 @@ class _RegisterPageState extends State<RegisterPage> {
: 'Échec de l\'inscription. Veuillez réessayer.');
if (isSuccess) {
if (mounted) {
if (context.mounted) {
// Afficher une boîte de dialogue de succès
showDialog(
context: context,
@@ -879,8 +879,7 @@ class _RegisterPageState extends State<RegisterPage> {
color: theme
.colorScheme
.onSurface
.withOpacity(
0.7),
.withValues(alpha: 0.7),
),
),
],
@@ -917,7 +916,7 @@ class _RegisterPageState extends State<RegisterPage> {
}
} else {
// Afficher le message d'erreur retourné par l'API
if (mounted) {
if (context.mounted) {
// Afficher un message d'erreur plus visible
showDialog(
context: context,
@@ -943,18 +942,20 @@ class _RegisterPageState extends State<RegisterPage> {
);
// Afficher également un SnackBar
ScaffoldMessenger.of(context)
if (context.mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
),
);
}
}
}
} else {
// Gérer les erreurs HTTP
if (mounted) {
if (context.mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
@@ -972,7 +973,7 @@ class _RegisterPageState extends State<RegisterPage> {
});
// Gérer les exceptions
if (mounted) {
if (context.mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
@@ -1078,17 +1079,17 @@ class _RegisterPageState extends State<RegisterPage> {
vertical: 4,
),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.1),
color: theme.colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: theme.colorScheme.primary.withOpacity(0.3),
color: theme.colorScheme.primary.withValues(alpha: 0.3),
width: 1,
),
),
child: Text(
'v$_appVersion',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.primary.withOpacity(0.8),
color: theme.colorScheme.primary.withValues(alpha: 0.8),
fontSize: 10,
fontWeight: FontWeight.w500,
),

View File

@@ -11,8 +11,8 @@ import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:shared_preferences/shared_preferences.dart';
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html if (dart.library.io) '';
// Import conditionnel pour le web
import 'package:universal_html/html.dart' as html;
class SplashPage extends StatefulWidget {
/// Action à effectuer après l'initialisation (login ou register)
@@ -32,7 +32,7 @@ class DotsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.5)
..color = Colors.white.withValues(alpha: 0.5)
..style = PaintingStyle.fill;
final random = math.Random(42); // Seed fixe pour consistance
@@ -521,6 +521,8 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
await Future.delayed(const Duration(milliseconds: 200));
if (!context.mounted) return;
switch (action) {
case 'login':
if (type == 'admin') {
@@ -617,7 +619,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
'Une application puissante et intuitive de gestion de vos distributions de calendriers',
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
fontWeight: FontWeight.w500,
),
),
@@ -637,7 +639,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: theme.colorScheme.primary.withOpacity(0.2),
color: theme.colorScheme.primary.withValues(alpha: 0.2),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -652,7 +654,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
builder: (context, value, child) {
return LinearProgressIndicator(
value: value,
backgroundColor: Colors.grey.withOpacity(0.15),
backgroundColor: Colors.grey.withValues(alpha: 0.15),
valueColor: AlwaysStoppedAnimation<Color>(
theme.colorScheme.primary,
),
@@ -682,7 +684,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
_statusMessage,
key: ValueKey(_statusMessage),
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
@@ -984,7 +986,7 @@ class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateM
vertical: 4,
),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.1),
color: theme.colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: theme.colorScheme.primary,

View File

@@ -1,7 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:geosector_app/chat/pages/rooms_page_embedded.dart';
import 'package:geosector_app/chat/chat_module.dart';
import 'package:geosector_app/core/services/chat_manager.dart';
import 'package:geosector_app/core/services/current_user_service.dart';
@@ -19,7 +18,6 @@ class _ChatCommunicationPageState extends State<ChatCommunicationPage> {
// Récupération du rôle de l'utilisateur
int get _userRole => CurrentUserService.instance.currentUser?.role ?? 1;
String get _userName => CurrentUserService.instance.userName ?? 'Utilisateur';
// Configuration selon le rôle
MaterialColor get _themeColor {
@@ -31,40 +29,10 @@ class _ChatCommunicationPageState extends State<ChatCommunicationPage> {
}
}
Color get _backgroundColor {
switch (_userRole) {
case 1: return Colors.green.shade50;
case 2: return Colors.red.shade50;
case 9: return Colors.blue.shade50;
default: return Colors.grey.shade50;
}
}
String get _pageTitle {
switch (_userRole) {
case 1: return 'Messages';
case 2: return 'Messages Administration';
case 9: return 'Centre de Communication GEOSECTOR';
default: return 'Messages';
}
}
IconData get _roleIcon {
switch (_userRole) {
case 1: return Icons.person;
case 2: return Icons.admin_panel_settings;
case 9: return Icons.shield;
default: return Icons.chat;
}
}
bool get _showStatsButton => _userRole == 9; // Super Admin uniquement
@override
Widget build(BuildContext context) {
// Détection de la plateforme
final isWeb = kIsWeb;
final isMobile = !isWeb;
// Construction adaptative
if (isWeb) {
@@ -80,25 +48,7 @@ class _ChatCommunicationPageState extends State<ChatCommunicationPage> {
return Scaffold(
backgroundColor: Colors.transparent,
body: Container(
margin: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withOpacity(0.1),
blurRadius: 20,
spreadRadius: 1,
offset: const Offset(0, 4),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: _buildContent(theme, isWeb: true),
),
),
body: _buildContent(theme, isWeb: true),
);
}
@@ -107,13 +57,6 @@ class _ChatCommunicationPageState extends State<ChatCommunicationPage> {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text(_pageTitle),
backgroundColor: _themeColor,
foregroundColor: Colors.white,
elevation: 2,
actions: _buildAppBarActions(),
),
body: _buildContent(theme, isWeb: false),
floatingActionButton: FloatingActionButton(
onPressed: _handleNewConversation,
@@ -138,13 +81,13 @@ class _ChatCommunicationPageState extends State<ChatCommunicationPage> {
Icon(
Icons.chat_bubble_outline,
size: 80,
color: _themeColor.withOpacity(0.3),
color: _themeColor.withValues(alpha: 0.3),
),
const SizedBox(height: 24),
Text(
'Module de communication non disponible',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.5),
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
textAlign: TextAlign.center,
),
@@ -152,7 +95,7 @@ class _ChatCommunicationPageState extends State<ChatCommunicationPage> {
Text(
_getUnavailableMessage(),
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.4),
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
),
textAlign: TextAlign.center,
),
@@ -176,19 +119,12 @@ class _ChatCommunicationPageState extends State<ChatCommunicationPage> {
// Le chat est initialisé
if (isWeb) {
// Version Web avec en-tête personnalisé
return Column(
children: [
_buildWebHeader(theme),
Expanded(
child: RoomsPageEmbedded(
key: _roomsPageKey,
onRefreshPressed: () {
debugPrint('Conversations actualisées');
},
),
),
],
// Version Web sans en-tête
return RoomsPageEmbedded(
key: _roomsPageKey,
onRefreshPressed: () {
debugPrint('Conversations actualisées');
},
);
} else {
// Version Mobile, contenu direct
@@ -201,84 +137,6 @@ class _ChatCommunicationPageState extends State<ChatCommunicationPage> {
}
}
/// En-tête personnalisé pour Web
Widget _buildWebHeader(ThemeData theme) {
return Container(
height: 60,
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: _backgroundColor,
border: Border(
bottom: BorderSide(
color: theme.dividerColor.withOpacity(0.1),
width: 1,
),
),
),
child: Row(
children: [
Icon(
_roleIcon,
color: _themeColor.shade600,
size: 24,
),
const SizedBox(width: 12),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_pageTitle,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
color: _themeColor.shade700,
),
),
if (_userRole == 9)
Text(
'Connecté en tant que $_userName',
style: theme.textTheme.bodySmall?.copyWith(
color: _themeColor.shade600,
),
),
],
),
),
// Boutons d'action
if (_userRole == 9) ...[
// Super Admin : Statistiques
TextButton.icon(
icon: Icon(Icons.analytics, color: _themeColor.shade600),
label: Text(
'Statistiques',
style: TextStyle(color: _themeColor.shade600),
),
onPressed: _handleShowStats,
),
],
],
),
);
}
/// Actions pour l'AppBar mobile
List<Widget> _buildAppBarActions() {
final actions = <Widget>[];
if (_showStatsButton) {
actions.add(
IconButton(
icon: const Icon(Icons.analytics),
onPressed: _handleShowStats,
tooltip: 'Statistiques',
),
);
}
return actions;
}
/// Message personnalisé selon le rôle quand le chat n'est pas disponible
String _getUnavailableMessage() {
switch (_userRole) {
@@ -298,17 +156,6 @@ class _ChatCommunicationPageState extends State<ChatCommunicationPage> {
_roomsPageKey.currentState?.createNewConversation();
}
void _handleShowStats() {
// TODO: Implémenter l'affichage des statistiques pour Super Admin
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Statistiques à venir...'),
backgroundColor: _themeColor,
),
);
}
void _handleRetryInit() async {
// Réessayer l'initialisation du chat (pour Super Admin)
await ChatManager.instance.reinitialize();

View File

@@ -119,9 +119,9 @@ class SectorActionResultDialog extends StatelessWidget {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
color: Colors.orange.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.withOpacity(0.3)),
border: Border.all(color: Colors.orange.withValues(alpha: 0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:geosector_app/core/data/models/sector_model.dart';
import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/core/data/models/user_sector_model.dart';
import 'package:geosector_app/core/repositories/membre_repository.dart';
import 'package:geosector_app/core/services/current_amicale_service.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
@@ -31,7 +30,6 @@ class _SectorDialogState extends State<SectorDialog> {
Color _selectedColor = Colors.blue;
final List<int> _selectedMemberIds = [];
bool _isLoading = false;
bool _membersLoaded = false;
String _searchQuery = '';
@override
@@ -96,12 +94,11 @@ class _SectorDialogState extends State<SectorDialog> {
// Marquer le chargement comme terminé
setState(() {
_membersLoaded = true;
});
} catch (e) {
debugPrint('Erreur lors du chargement des membres du secteur: $e');
setState(() {
_membersLoaded = true; // Même en cas d'erreur
// Marquer comme terminé même en cas d'erreur
});
}
}
@@ -121,7 +118,7 @@ class _SectorDialogState extends State<SectorDialog> {
}
String _colorToHex(Color color) {
return '#${color.value.toRadixString(16).substring(2).toUpperCase()}';
return '#${color.toARGB32().toRadixString(16).substring(2).toUpperCase()}';
}
void _handleSave() async {
@@ -200,7 +197,7 @@ class _SectorDialogState extends State<SectorDialog> {
itemCount: colors.length,
itemBuilder: (context, index) {
final color = colors[index];
final isSelected = _selectedColor.value == color.value;
final isSelected = _selectedColor.toARGB32() == color.toARGB32();
return InkWell(
onTap: () {
@@ -222,7 +219,7 @@ class _SectorDialogState extends State<SectorDialog> {
boxShadow: isSelected
? [
BoxShadow(
color: Colors.black.withOpacity(0.3),
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),

View File

@@ -102,10 +102,10 @@ class ThemeSettingsPage extends StatelessWidget {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer.withOpacity(0.3),
color: theme.colorScheme.primaryContainer.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: theme.colorScheme.primary.withOpacity(0.3),
color: theme.colorScheme.primary.withValues(alpha: 0.3),
),
),
child: Row(
@@ -287,7 +287,7 @@ class ThemeSettingsPage extends StatelessWidget {
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.withOpacity(0.3)),
border: Border.all(color: Colors.grey.withValues(alpha: 0.3)),
),
child: Center(
child: Text(

View File

@@ -40,7 +40,8 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
if (operation != null) {
return Text(
'${operation.name} (${_formatDate(operation.dateDebut)}-${_formatDate(operation.dateFin)})',
style: theme.textTheme.headlineMedium?.copyWith(
style: TextStyle(
fontSize: AppTheme.r(context, 20),
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
@@ -48,7 +49,8 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
} else {
return Text(
'Tableau de bord',
style: theme.textTheme.headlineMedium?.copyWith(
style: TextStyle(
fontSize: AppTheme.r(context, 20),
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
@@ -88,46 +90,45 @@ class _UserDashboardHomePageState extends State<UserDashboardHomePage> {
}
// Construction d'une carte combinée pour les règlements (liste + graphique)
Widget _buildCombinedPaymentsCard(bool isDesktop) {
return PaymentSummaryCard(
title: 'Mes règlements',
titleColor: AppTheme.accentColor,
titleIcon: Icons.payments,
height: 300,
useValueListenable: true,
userId: userRepository.getCurrentUser()?.id,
showAllPayments: false,
isDesktop: isDesktop,
backgroundIcon: Icons.euro_symbol,
backgroundIconColor: Colors.blue,
backgroundIconOpacity: 0.07,
backgroundIconSize: 180,
customTotalDisplay: (totalAmount) {
// Calculer le nombre de passages avec règlement pour le titre personnalisé
final currentUser = userRepository.getCurrentUser();
if (currentUser == null) return '${totalAmount.toStringAsFixed(2)}';
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
int passagesCount = 0;
for (final passage in passagesBox.values) {
if (passage.fkUser == currentUser.id) {
double montant = 0.0;
try {
String montantStr = passage.montant.replaceAll(',', '.');
montant = double.tryParse(montantStr) ?? 0.0;
} catch (e) {
// Ignorer les erreurs
}
if (montant > 0) passagesCount++;
}
}
return '${totalAmount.toStringAsFixed(2)} € sur $passagesCount passages';
},
);
}
Widget _buildCombinedPaymentsCard(bool isDesktop) {
return PaymentSummaryCard(
title: 'Mes règlements',
titleColor: AppTheme.accentColor,
titleIcon: Icons.payments,
height: 300,
useValueListenable: true,
userId: userRepository.getCurrentUser()?.id,
showAllPayments: false,
isDesktop: isDesktop,
backgroundIcon: Icons.euro_symbol,
backgroundIconColor: Colors.blue,
backgroundIconOpacity: 0.07,
backgroundIconSize: 180,
customTotalDisplay: (totalAmount) {
// Calculer le nombre de passages avec règlement pour le titre personnalisé
final currentUser = userRepository.getCurrentUser();
if (currentUser == null) return '${totalAmount.toStringAsFixed(2)}';
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
int passagesCount = 0;
for (final passage in passagesBox.values) {
if (passage.fkUser == currentUser.id) {
double montant = 0.0;
try {
String montantStr = passage.montant.replaceAll(',', '.');
montant = double.tryParse(montantStr) ?? 0.0;
} catch (e) {
// Ignorer les erreurs
}
if (montant > 0) passagesCount++;
}
}
return '${totalAmount.toStringAsFixed(2)} € sur $passagesCount passages';
},
);
}
// Construction d'une carte combinée pour les passages (liste + graphique)
Widget _buildCombinedPassagesCard(BuildContext context, bool isDesktop) {
@@ -136,8 +137,8 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
titleColor: AppTheme.primaryColor,
titleIcon: Icons.route,
height: 300,
useValueListenable: true,
userId: userRepository.getCurrentUser()?.id,
useValueListenable: true,
userId: userRepository.getCurrentUser()?.id,
showAllPassages: false,
excludePassageTypes: const [2], // Exclure "À finaliser"
isDesktop: isDesktop,
@@ -160,7 +161,9 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
height: 350,
child: ActivityChart(
useValueListenable: true, // Utiliser le système réactif
excludePassageTypes: const [2], // Exclure les passages "À finaliser"
excludePassageTypes: const [
2
], // Exclure les passages "À finaliser"
daysToShow: 15,
periodType: 'Jour',
height: 350,
@@ -178,27 +181,29 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
Widget _buildRecentPassages(BuildContext context, ThemeData theme) {
// Utilisation directe du widget PassagesListWidget sans Card wrapper
return ValueListenableBuilder(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
valueListenable:
Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
final recentPassages = _getRecentPassages(passagesBox);
// Debug : afficher le nombre de passages récupérés
debugPrint('UserDashboardHomePage: ${recentPassages.length} passages récents récupérés');
debugPrint(
'UserDashboardHomePage: ${recentPassages.length} passages récents récupérés');
if (recentPassages.isEmpty) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: const Padding(
child: Padding(
padding: EdgeInsets.all(32.0),
child: Center(
child: Text(
'Aucun passage récent',
style: TextStyle(
color: Colors.grey,
fontSize: 14,
fontSize: AppTheme.r(context, 14),
),
),
),
@@ -208,7 +213,8 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
// Utiliser une hauteur fixe pour le widget dans le dashboard
return SizedBox(
height: 450, // Hauteur légèrement augmentée pour compenser l'absence de Card
height:
450, // Hauteur légèrement augmentée pour compenser l'absence de Card
child: PassagesListWidget(
passages: recentPassages,
showFilters: false,
@@ -217,8 +223,10 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
maxPassages: 20,
// Ne pas appliquer de filtres supplémentaires car les passages
// sont déjà filtrés dans _getRecentPassages
excludePassageTypes: null, // Pas de filtre, déjà géré dans _getRecentPassages
filterByUserId: null, // Pas de filtre, déjà géré dans _getRecentPassages
excludePassageTypes:
null, // Pas de filtre, déjà géré dans _getRecentPassages
filterByUserId:
null, // Pas de filtre, déjà géré dans _getRecentPassages
periodFilter: null, // Pas de filtre de période
// Le widget gère maintenant le flux conditionnel par défaut
onPassageSelected: null,
@@ -245,18 +253,19 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
/// Récupère les passages récents pour la liste
List<Map<String, dynamic>> _getRecentPassages(Box<PassageModel> passagesBox) {
final currentUserId = userRepository.getCurrentUser()?.id;
// Filtrer les passages :
// Filtrer les passages :
// - Avoir une date passedAt
// - Exclure le type 2 ("À finaliser")
// - Appartenir à l'utilisateur courant
final allPassages = passagesBox.values.where((p) {
if (p.passedAt == null) return false;
if (p.fkType == 2) return false; // Exclure les passages "À finaliser"
if (currentUserId != null && p.fkUser != currentUserId) return false; // Filtrer par utilisateur
if (currentUserId != null && p.fkUser != currentUserId)
return false; // Filtrer par utilisateur
return true;
}).toList();
// Trier par date décroissante
allPassages.sort((a, b) => b.passedAt!.compareTo(a.passedAt!));
@@ -294,7 +303,10 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
'hasReceipt': passage.nomRecu.isNotEmpty,
'hasError': passage.emailErreur.isNotEmpty,
'fkUser': passage.fkUser,
'isOwnedByCurrentUser': passage.fkUser == userRepository.getCurrentUser()?.id, // Ajout du champ pour le widget
'isOwnedByCurrentUser': passage.fkUser ==
userRepository
.getCurrentUser()
?.id, // Ajout du champ pour le widget
};
}).toList();
}

View File

@@ -190,7 +190,7 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withOpacity(0.1),
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
@@ -217,7 +217,7 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
Text(
'Vous n\'avez pas encore été affecté à une opération. Veuillez contacter votre administrateur pour obtenir un accès.',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
@@ -240,7 +240,7 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withOpacity(0.1),
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
@@ -267,7 +267,7 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
Text(
'Vous n\'êtes affecté sur aucun secteur. Contactez votre administrateur pour qu\'il vous en affecte au moins un.',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_map/flutter_map.dart';
@@ -10,7 +11,6 @@ import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/services/current_amicale_service.dart';
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
@@ -26,36 +26,37 @@ class UserFieldModePage extends StatefulWidget {
State<UserFieldModePage> createState() => _UserFieldModePageState();
}
class _UserFieldModePageState extends State<UserFieldModePage> with TickerProviderStateMixin {
class _UserFieldModePageState extends State<UserFieldModePage>
with TickerProviderStateMixin {
// Controllers
final MapController _mapController = MapController();
final TextEditingController _searchController = TextEditingController();
// Animation controllers pour le clignotement
late AnimationController _gpsBlinkController;
late AnimationController _networkBlinkController;
late Animation<double> _gpsBlinkAnimation;
late Animation<double> _networkBlinkAnimation;
// Position et tracking
Position? _currentPosition;
StreamSubscription<Position>? _positionStreamSubscription;
Timer? _qualityUpdateTimer;
// Qualité des signaux
double _gpsAccuracy = 999;
ConnectivityResult _connectivityResult = ConnectivityResult.none;
bool _isGpsEnabled = false;
// Mode boussole
bool _compassMode = false;
double _heading = 0;
StreamSubscription<MagnetometerEvent>? _magnetometerSubscription;
// Filtrage et recherche
String _searchQuery = '';
List<PassageModel> _nearbyPassages = [];
// État de chargement
bool _isLoading = true;
bool _locationPermissionGranted = false;
@@ -65,7 +66,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
void initState() {
super.initState();
_initializeAnimations();
if (kIsWeb) {
// Sur web, utiliser une position simulée pour éviter le blocage
_initializeWebMode();
@@ -75,19 +76,21 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
_startQualityMonitoring();
}
}
void _initializeWebMode() async {
// Essayer d'obtenir la position réelle depuis le navigateur
try {
setState(() {
_statusMessage = "Demande d'autorisation de géolocalisation...";
});
// Demander la permission et obtenir la position
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
),
);
setState(() {
_currentPosition = position;
_gpsAccuracy = position.accuracy;
@@ -97,38 +100,40 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
_locationPermissionGranted = true;
_statusMessage = "";
});
// Charger les passages proches de la position réelle
_updateNearbyPassages();
// Démarrer le suivi de position même sur web
_startLocationTracking();
} catch (e) {
debugPrint('Erreur géolocalisation web: $e');
// Essayer d'utiliser les coordonnées GPS de l'amicale
double fallbackLat = 46.603354; // Centre de la France par défaut
double fallbackLat = 46.603354; // Centre de la France par défaut
double fallbackLng = 1.888334;
String statusMessage = "Position approximative";
try {
final amicale = CurrentAmicaleService.instance.currentAmicale;
if (amicale != null && amicale.gpsLat.isNotEmpty && amicale.gpsLng.isNotEmpty) {
if (amicale != null &&
amicale.gpsLat.isNotEmpty &&
amicale.gpsLng.isNotEmpty) {
final amicaleLat = double.tryParse(amicale.gpsLat);
final amicaleLng = double.tryParse(amicale.gpsLng);
if (amicaleLat != null && amicaleLng != null) {
fallbackLat = amicaleLat;
fallbackLng = amicaleLng;
statusMessage = "Position de l'amicale";
debugPrint('Utilisation des coordonnées de l\'amicale: $fallbackLat, $fallbackLng');
debugPrint(
'Utilisation des coordonnées de l\'amicale: $fallbackLat, $fallbackLng');
}
}
} catch (amicaleError) {
debugPrint('Erreur récupération coordonnées amicale: $amicaleError');
}
// Utiliser la position de fallback (amicale ou centre France)
setState(() {
_currentPosition = Position(
@@ -150,7 +155,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
_locationPermissionGranted = false;
_statusMessage = statusMessage;
});
_updateNearbyPassages();
}
}
@@ -207,12 +212,12 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
_isGpsEnabled = true;
_isLoading = false;
});
_updateNearbyPassages();
_updateBlinkAnimations();
// Centrer la carte sur la nouvelle position
if (_mapController.mapEventStream != null && !_compassMode) {
if (!_compassMode) {
_mapController.move(LatLng(position.latitude, position.longitude), 17);
}
}, onError: (error) {
@@ -224,22 +229,24 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
void _startQualityMonitoring() {
// Mise à jour toutes les 5 secondes
_qualityUpdateTimer = Timer.periodic(const Duration(seconds: 5), (timer) async {
_qualityUpdateTimer =
Timer.periodic(const Duration(seconds: 5), (timer) async {
// Vérifier la connexion réseau
final connectivityResults = await Connectivity().checkConnectivity();
setState(() {
// Prendre le premier résultat de la liste
_connectivityResult = connectivityResults.isNotEmpty
? connectivityResults.first
_connectivityResult = connectivityResults.isNotEmpty
? connectivityResults.first
: ConnectivityResult.none;
});
// Vérifier si le GPS est activé
final isLocationServiceEnabled = await Geolocator.isLocationServiceEnabled();
final isLocationServiceEnabled =
await Geolocator.isLocationServiceEnabled();
setState(() {
_isGpsEnabled = isLocationServiceEnabled;
});
_updateBlinkAnimations();
});
}
@@ -272,9 +279,9 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
// Calculer les distances et trier
final passagesWithDistance = allPassages.map((passage) {
// Convertir les coordonnées GPS string en double
final double lat = double.tryParse(passage.gpsLat ?? '0') ?? 0;
final double lng = double.tryParse(passage.gpsLng ?? '0') ?? 0;
final double lat = double.tryParse(passage.gpsLat) ?? 0;
final double lng = double.tryParse(passage.gpsLng) ?? 0;
final distance = _calculateDistance(
_currentPosition!.latitude,
_currentPosition!.longitude,
@@ -295,7 +302,8 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
});
}
double _calculateDistance(double lat1, double lon1, double lat2, double lon2) {
double _calculateDistance(
double lat1, double lon1, double lat2, double lon2) {
const distance = Distance();
return distance.as(
LengthUnit.Meter,
@@ -315,7 +323,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
);
return;
}
setState(() {
_compassMode = !_compassMode;
});
@@ -330,7 +338,8 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
}
void _startCompass() {
_magnetometerSubscription = magnetometerEvents.listen((MagnetometerEvent event) {
_magnetometerSubscription =
magnetometerEventStream().listen((MagnetometerEvent event) {
setState(() {
// Calculer l'orientation à partir du magnétomètre
_heading = math.atan2(event.y, event.x) * (180 / math.pi);
@@ -346,7 +355,6 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
});
}
void _recenterMap() {
if (_currentPosition != null) {
_mapController.move(
@@ -374,7 +382,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
),
);
}
// Vérifier si l'amicale autorise la suppression des passages
bool _canDeletePassages() {
try {
@@ -383,17 +391,19 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
return amicale.chkUserDeletePass == true;
}
} catch (e) {
debugPrint('Erreur lors de la vérification des permissions de suppression: $e');
debugPrint(
'Erreur lors de la vérification des permissions de suppression: $e');
}
return false;
}
// Afficher le dialog de confirmation de suppression
void _showDeleteConfirmationDialog(PassageModel passage) {
final TextEditingController confirmController = TextEditingController();
final String streetNumber = passage.numero ?? '';
final String fullAddress = '${passage.numero ?? ''} ${passage.rueBis ?? ''} ${passage.rue ?? ''}'.trim();
final String streetNumber = passage.numero;
final String fullAddress =
'${passage.numero} ${passage.rueBis} ${passage.rue}'.trim();
showDialog(
context: context,
barrierDismissible: false,
@@ -411,12 +421,12 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
Text(
'ATTENTION : Cette action est irréversible !',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red,
fontSize: 16,
fontSize: AppTheme.r(context, 16),
),
),
const SizedBox(height: 16),
@@ -434,9 +444,9 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
),
child: Text(
fullAddress.isEmpty ? 'Adresse inconnue' : fullAddress,
style: const TextStyle(
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
fontSize: AppTheme.r(context, 14),
),
),
),
@@ -450,7 +460,9 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
controller: confirmController,
decoration: InputDecoration(
labelText: 'Numéro de rue',
hintText: streetNumber.isNotEmpty ? 'Ex: $streetNumber' : 'Saisir le numéro',
hintText: streetNumber.isNotEmpty
? 'Ex: $streetNumber'
: 'Saisir le numéro',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.home),
),
@@ -481,8 +493,9 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
);
return;
}
if (streetNumber.isNotEmpty && enteredNumber.toUpperCase() != streetNumber.toUpperCase()) {
if (streetNumber.isNotEmpty &&
enteredNumber.toUpperCase() != streetNumber.toUpperCase()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Le numéro de rue ne correspond pas'),
@@ -491,11 +504,11 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
);
return;
}
// Fermer le dialog
confirmController.dispose();
Navigator.of(dialogContext).pop();
// Effectuer la suppression
await _deletePassage(passage);
},
@@ -510,20 +523,21 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
},
);
}
// Supprimer un passage
Future<void> _deletePassage(PassageModel passage) async {
try {
// Appeler le repository pour supprimer via l'API
final success = await passageRepository.deletePassageViaApi(passage.id);
if (success && mounted) {
ApiException.showSuccess(context, 'Passage supprimé avec succès');
// Rafraîchir la liste des passages
_updateNearbyPassages();
} else if (mounted) {
ApiException.showError(context, Exception('Erreur lors de la suppression'));
ApiException.showError(
context, Exception('Erreur lors de la suppression'));
}
} catch (e) {
debugPrint('Erreur suppression passage: $e');
@@ -546,8 +560,6 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
@@ -558,19 +570,22 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
const SizedBox(width: 16),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
kIsWeb
? (_locationPermissionGranted
? 'GPS: ${_currentPosition!.latitude.toStringAsFixed(4)}, ${_currentPosition!.longitude.toStringAsFixed(4)}'
: _statusMessage.isNotEmpty ? _statusMessage : 'Position approximative')
: '',
style: const TextStyle(
fontSize: 12,
kIsWeb
? (_locationPermissionGranted
? 'GPS: ${_currentPosition!.latitude.toStringAsFixed(4)}, ${_currentPosition!.longitude.toStringAsFixed(4)}'
: _statusMessage.isNotEmpty
? _statusMessage
: 'Position approximative')
: '',
style: TextStyle(
fontSize: AppTheme.r(context, 12),
fontWeight: FontWeight.normal,
),
overflow: TextOverflow.ellipsis,
@@ -636,7 +651,8 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
),
filled: true,
fillColor: Colors.grey[100],
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
contentPadding:
const EdgeInsets.symmetric(horizontal: 20),
),
onChanged: (value) {
setState(() {
@@ -648,10 +664,12 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
// En-tête de la liste
Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Icon(Icons.location_on, color: Colors.green[600], size: 20),
Icon(Icons.location_on,
color: Colors.green[600], size: 20),
const SizedBox(width: 8),
Text(
'${_getFilteredPassages().length} passage${_getFilteredPassages().length > 1 ? 's' : ''} à proximité',
@@ -709,7 +727,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
color: color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
@@ -719,7 +737,10 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
const SizedBox(width: 4),
Text(
'${_gpsAccuracy.toStringAsFixed(0)}m',
style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 12),
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: AppTheme.r(context, 12)),
),
],
),
@@ -774,7 +795,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
color: color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(20),
),
child: Row(
@@ -784,7 +805,10 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
const SizedBox(width: 4),
Text(
label,
style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 12),
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: AppTheme.r(context, 12)),
),
],
),
@@ -806,7 +830,8 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
}
final apiService = ApiService.instance;
final mapboxApiKey = AppKeys.getMapboxApiKey(apiService.getCurrentEnvironment());
final mapboxApiKey =
AppKeys.getMapboxApiKey(apiService.getCurrentEnvironment());
return Stack(
children: [
@@ -815,7 +840,8 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
child: FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: LatLng(_currentPosition!.latitude, _currentPosition!.longitude),
initialCenter: LatLng(
_currentPosition!.latitude, _currentPosition!.longitude),
initialZoom: 17,
maxZoom: 19,
minZoom: 10,
@@ -827,9 +853,9 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
children: [
TileLayer(
// Utiliser l'API v4 de Mapbox sur mobile ou OpenStreetMap en fallback
urlTemplate: kIsWeb
? 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token=$mapboxApiKey'
: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', // OpenStreetMap temporairement sur mobile
urlTemplate: kIsWeb
? 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token=$mapboxApiKey'
: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', // OpenStreetMap temporairement sur mobile
userAgentPackageName: 'app.geosector.fr',
additionalOptions: const {
'attribution': '© OpenStreetMap contributors',
@@ -840,24 +866,27 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
CircleLayer(
circles: [
CircleMarker(
point: LatLng(_currentPosition!.latitude, _currentPosition!.longitude),
point: LatLng(_currentPosition!.latitude,
_currentPosition!.longitude),
radius: 50,
color: Colors.blue.withOpacity(0.1),
borderColor: Colors.blue.withOpacity(0.3),
color: Colors.blue.withValues(alpha: 0.1),
borderColor: Colors.blue.withValues(alpha: 0.3),
borderStrokeWidth: 1,
),
CircleMarker(
point: LatLng(_currentPosition!.latitude, _currentPosition!.longitude),
point: LatLng(_currentPosition!.latitude,
_currentPosition!.longitude),
radius: 100,
color: Colors.transparent,
borderColor: Colors.blue.withOpacity(0.2),
borderColor: Colors.blue.withValues(alpha: 0.2),
borderStrokeWidth: 1,
),
CircleMarker(
point: LatLng(_currentPosition!.latitude, _currentPosition!.longitude),
point: LatLng(_currentPosition!.latitude,
_currentPosition!.longitude),
radius: 250,
color: Colors.transparent,
borderColor: Colors.blue.withOpacity(0.15),
borderColor: Colors.blue.withValues(alpha: 0.15),
borderStrokeWidth: 1,
),
],
@@ -870,7 +899,8 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
MarkerLayer(
markers: [
Marker(
point: LatLng(_currentPosition!.latitude, _currentPosition!.longitude),
point: LatLng(_currentPosition!.latitude,
_currentPosition!.longitude),
width: 30,
height: 30,
child: Container(
@@ -880,7 +910,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
border: Border.all(color: Colors.white, width: 3),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.3),
color: Colors.blue.withValues(alpha: 0.3),
blurRadius: 10,
spreadRadius: 5,
),
@@ -941,9 +971,9 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
const SizedBox(width: 4),
Text(
'Mode boussole',
style: const TextStyle(
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontSize: AppTheme.r(context, 12),
fontWeight: FontWeight.bold,
),
),
@@ -973,9 +1003,9 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
const borderColor = Color(0xFFF7A278);
// Convertir les coordonnées GPS string en double
final double lat = double.tryParse(passage.gpsLat ?? '0') ?? 0;
final double lng = double.tryParse(passage.gpsLng ?? '0') ?? 0;
final double lat = double.tryParse(passage.gpsLat) ?? 0;
final double lng = double.tryParse(passage.gpsLng) ?? 0;
return Marker(
point: LatLng(lat, lng),
width: 40,
@@ -989,7 +1019,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
border: Border.all(color: borderColor, width: 3),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
@@ -997,11 +1027,12 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
),
child: Center(
child: Text(
'${passage.numero ?? ''}${(passage.rueBis != null && passage.rueBis!.isNotEmpty) ? passage.rueBis!.substring(0, 1).toLowerCase() : ''}',
'${passage.numero}${(passage.rueBis.isNotEmpty) ? passage.rueBis.substring(0, 1).toLowerCase() : ''}',
style: TextStyle(
color: fillColor == Colors.white ? Colors.black : Colors.white,
color:
fillColor == Colors.white ? Colors.black : Colors.white,
fontWeight: FontWeight.bold,
fontSize: 12,
fontSize: AppTheme.r(context, 12),
),
textAlign: TextAlign.center,
maxLines: 1,
@@ -1016,19 +1047,21 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
List<Map<String, dynamic>> _getFilteredPassages() {
// Filtrer d'abord par recherche si nécessaire
List<PassageModel> filtered = _searchQuery.isEmpty
? _nearbyPassages
: _nearbyPassages.where((passage) {
final address = '${passage.numero ?? ''} ${passage.rueBis ?? ''} ${passage.rue ?? ''}'.trim().toLowerCase();
return address.contains(_searchQuery);
}).toList();
List<PassageModel> filtered = _searchQuery.isEmpty
? _nearbyPassages
: _nearbyPassages.where((passage) {
final address = '${passage.numero} ${passage.rueBis} ${passage.rue}'
.trim()
.toLowerCase();
return address.contains(_searchQuery);
}).toList();
// Convertir au format attendu par PassagesListWidget avec distance
return filtered.map((passage) {
// Calculer la distance
final double lat = double.tryParse(passage.gpsLat ?? '0') ?? 0;
final double lng = double.tryParse(passage.gpsLng ?? '0') ?? 0;
final double lat = double.tryParse(passage.gpsLat) ?? 0;
final double lng = double.tryParse(passage.gpsLng) ?? 0;
final distance = _currentPosition != null
? _calculateDistance(
_currentPosition!.latitude,
@@ -1037,10 +1070,11 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
lng,
)
: 0.0;
// Construire l'adresse complète
final String address = '${passage.numero ?? ''} ${passage.rueBis ?? ''} ${passage.rue ?? ''}'.trim();
final String address =
'${passage.numero} ${passage.rueBis} ${passage.rue}'.trim();
// Convertir le montant
double amount = 0.0;
try {
@@ -1051,7 +1085,7 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
} catch (e) {
// Ignorer les erreurs de conversion
}
return {
'id': passage.id,
'address': address.isEmpty ? 'Adresse inconnue' : address,
@@ -1066,7 +1100,10 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
'fkUser': passage.fkUser,
'distance': distance, // Ajouter la distance pour le tri et l'affichage
'nbPassages': passage.nbPassages, // Pour la couleur de l'indicateur
'isOwnedByCurrentUser': passage.fkUser == userRepository.getCurrentUser()?.id, // Ajout du champ pour le widget
'isOwnedByCurrentUser': passage.fkUser ==
userRepository
.getCurrentUser()
?.id, // Ajout du champ pour le widget
// Garder les données originales pour l'édition
'numero': passage.numero,
'rueBis': passage.rueBis,
@@ -1109,18 +1146,18 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
},
);
},
onPassageDelete: _canDeletePassages()
? (passage) {
// Retrouver le PassageModel original pour la suppression
final passageId = passage['id'] as int;
final originalPassage = _nearbyPassages.firstWhere(
(p) => p.id == passageId,
orElse: () => _nearbyPassages.first,
);
_showDeleteConfirmationDialog(originalPassage);
}
: null,
onPassageDelete: _canDeletePassages()
? (passage) {
// Retrouver le PassageModel original pour la suppression
final passageId = passage['id'] as int;
final originalPassage = _nearbyPassages.firstWhere(
(p) => p.id == passageId,
orElse: () => _nearbyPassages.first,
);
_showDeleteConfirmationDialog(originalPassage);
}
: null,
),
);
}
}
}

View File

@@ -1,5 +1,6 @@
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:flutter/material.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
@@ -664,7 +665,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
color: Colors.white.withOpacity(0.95),
color: Colors.white.withValues(alpha: 0.95),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
@@ -868,56 +869,15 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec bouton de rafraîchissement
// Filtres avec bouton de rafraîchissement
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_isLoading
? 'Historique des passages'
: 'Historique des ${_convertedPassages.length} passages${_totalSectors > 0 ? ' ($_totalSectors secteur${_totalSectors > 1 ? 's' : ''})' : ''}',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
if (!_isLoading && _sharedMembersCount > 0)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
'Partagés avec $_sharedMembersCount membre${_sharedMembersCount > 1 ? 's' : ''}',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
fontStyle: FontStyle.italic,
),
),
),
],
),
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadPassages,
tooltip: 'Rafraîchir',
),
],
),
// Filtres (secteur et période)
// Filtres (secteur et période) avec bouton rafraîchir
if (!_isLoading && (_userSectors.length > 1 || selectedPeriod != 'Tous'))
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: _buildFilters(context),
),
_buildFilters(context),
],
),
),
@@ -940,8 +900,9 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
const SizedBox(height: 16),
Text(
'Erreur de chargement',
style: theme.textTheme.titleLarge
?.copyWith(color: Colors.red),
style: TextStyle(
fontSize: AppTheme.r(context, 22),
color: Colors.red),
),
const SizedBox(height: 8),
Text(_errorMessage),
@@ -1019,7 +980,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
color: _currentSort == PassageSortType.dateDesc ||
_currentSort == PassageSortType.dateAsc
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withOpacity(0.6),
: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
tooltip: _currentSort == PassageSortType.dateAsc
? 'Tri par date (ancien en premier)'
@@ -1053,7 +1014,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
color: _currentSort == PassageSortType.addressDesc ||
_currentSort == PassageSortType.addressAsc
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withOpacity(0.6),
: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
tooltip: _currentSort == PassageSortType.addressAsc
? 'Tri par adresse (A-Z)'

View File

@@ -37,9 +37,6 @@ class _UserMapPageState extends State<UserMapPage> {
final List<Map<String, dynamic>> _sectors = [];
final List<Map<String, dynamic>> _passages = [];
// État du plein écran
bool _isFullScreen = false;
// Items pour la combobox de secteurs
List<DropdownMenuItem<int?>> _sectorItems = [];
@@ -567,32 +564,12 @@ class _UserMapPageState extends State<UserMapPage> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final size = MediaQuery.of(context).size;
final isDesktop = size.width > 900;
return Scaffold(
backgroundColor: Colors.transparent,
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête - affiché uniquement si pas en plein écran
if (!_isFullScreen)
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Carte des passages',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
),
// Filtres - affichés uniquement si pas en plein écran
if (!_isFullScreen) _buildFilters(theme, isDesktop),
// Carte
Expanded(
child: Stack(
@@ -606,7 +583,7 @@ class _UserMapPageState extends State<UserMapPage> {
useOpenStreetMap: !kIsWeb,
markers: _buildPassageMarkers(),
polygons: _buildSectorPolygons(),
showControls: true,
showControls: false, // Désactiver les contrôles par défaut pour éviter la duplication
onMapEvent: (event) {
if (event is MapEventMove) {
// Mettre à jour la position et le zoom actuels
@@ -632,7 +609,7 @@ class _UserMapPageState extends State<UserMapPage> {
width:
220, // Largeur fixe pour accommoder les noms longs
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.95),
color: Colors.white.withValues(alpha: 0.95),
borderRadius: BorderRadius.circular(8),
),
child: Row(
@@ -673,31 +650,148 @@ class _UserMapPageState extends State<UserMapPage> {
),
),
// Bouton de plein écran (les autres contrôles sont gérés par MapboxMap)
// Contrôles de zoom et localisation en bas à droite
Positioned(
bottom: 16.0,
right: 16.0,
child: _buildMapButton(
icon: _isFullScreen
? Icons.fullscreen_exit
: Icons.fullscreen,
onPressed: () {
setState(() {
_isFullScreen = !_isFullScreen;
});
},
child: Column(
children: [
// Bouton zoom +
_buildMapButton(
icon: Icons.add,
onPressed: () {
final newZoom = _currentZoom + 1;
_mapController.move(_currentPosition, newZoom);
setState(() {
_currentZoom = newZoom;
});
_saveSettings();
},
),
const SizedBox(height: 8),
// Bouton zoom -
_buildMapButton(
icon: Icons.remove,
onPressed: () {
final newZoom = _currentZoom - 1;
_mapController.move(_currentPosition, newZoom);
setState(() {
_currentZoom = newZoom;
});
_saveSettings();
},
),
const SizedBox(height: 8),
// Bouton de localisation
_buildMapButton(
icon: Icons.my_location,
onPressed: () {
_getUserLocation();
},
),
],
),
),
// Bouton de localisation personnalisé (pour utiliser notre propre logique)
// Filtres de type de passage en bas à gauche
Positioned(
bottom: 80.0, // Positionné au-dessus du bouton plein écran
right: 16.0,
child: _buildMapButton(
icon: Icons.my_location,
onPressed: () {
_getUserLocation();
},
bottom: 16.0,
left: 16.0,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Filtre Effectués (type 1)
_buildFilterDot(
color: Color(AppKeys.typesPassages[1]?['couleur2'] as int),
selected: _showEffectues,
onTap: () {
setState(() {
_showEffectues = !_showEffectues;
_loadPassages();
_saveSettings();
});
},
),
const SizedBox(width: 6),
// Filtre À finaliser (type 2)
_buildFilterDot(
color: Color(AppKeys.typesPassages[2]?['couleur2'] as int),
selected: _showAFinaliser,
onTap: () {
setState(() {
_showAFinaliser = !_showAFinaliser;
_loadPassages();
_saveSettings();
});
},
),
const SizedBox(width: 6),
// Filtre Refusés (type 3)
_buildFilterDot(
color: Color(AppKeys.typesPassages[3]?['couleur2'] as int),
selected: _showRefuses,
onTap: () {
setState(() {
_showRefuses = !_showRefuses;
_loadPassages();
_saveSettings();
});
},
),
const SizedBox(width: 6),
// Filtre Dons (type 4)
_buildFilterDot(
color: Color(AppKeys.typesPassages[4]?['couleur2'] as int),
selected: _showDons,
onTap: () {
setState(() {
_showDons = !_showDons;
_loadPassages();
_saveSettings();
});
},
),
const SizedBox(width: 6),
// Filtre Lots (type 5)
_buildFilterDot(
color: Color(AppKeys.typesPassages[5]?['couleur2'] as int),
selected: _showLots,
onTap: () {
setState(() {
_showLots = !_showLots;
_loadPassages();
_saveSettings();
});
},
),
const SizedBox(width: 6),
// Filtre Maisons vides (type 6)
_buildFilterDot(
color: Color(AppKeys.typesPassages[6]?['couleur2'] as int),
selected: _showMaisonsVides,
onTap: () {
setState(() {
_showMaisonsVides = !_showMaisonsVides;
_loadPassages();
_saveSettings();
});
},
),
],
),
),
),
],
@@ -709,145 +803,26 @@ class _UserMapPageState extends State<UserMapPage> {
);
}
// Construire les filtres pour les passages
Widget _buildFilters(ThemeData theme, bool isDesktop) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
// Filtre pour les passages effectués
_buildFilterChip(
label: AppKeys.typesPassages[1]?['titres'] as String? ??
'Effectués',
selected: _showEffectues,
color: Color(AppKeys.typesPassages[1]?['couleur2'] as int),
onSelected: (selected) {
setState(() {
_showEffectues = selected;
_loadPassages(); // Recharger les passages avec le nouveau filtre
_saveSettings(); // Sauvegarder les préférences
});
},
),
// Filtre pour les passages à finaliser
_buildFilterChip(
label: AppKeys.typesPassages[2]?['titres'] as String? ??
'À finaliser',
selected: _showAFinaliser,
color: Color(AppKeys.typesPassages[2]?['couleur2'] as int),
onSelected: (selected) {
setState(() {
_showAFinaliser = selected;
_loadPassages(); // Recharger les passages avec le nouveau filtre
_saveSettings(); // Sauvegarder les préférences
});
},
),
// Filtre pour les passages refusés
_buildFilterChip(
label:
AppKeys.typesPassages[3]?['titres'] as String? ?? 'Refusés',
selected: _showRefuses,
color: Color(AppKeys.typesPassages[3]?['couleur2'] as int),
onSelected: (selected) {
setState(() {
_showRefuses = selected;
_loadPassages(); // Recharger les passages avec le nouveau filtre
_saveSettings(); // Sauvegarder les préférences
});
},
),
// Filtre pour les dons
_buildFilterChip(
label: AppKeys.typesPassages[4]?['titres'] as String? ?? 'Dons',
selected: _showDons,
color: Color(AppKeys.typesPassages[4]?['couleur2'] as int),
onSelected: (selected) {
setState(() {
_showDons = selected;
_loadPassages(); // Recharger les passages avec le nouveau filtre
_saveSettings(); // Sauvegarder les préférences
});
},
),
// Filtre pour les lots
_buildFilterChip(
label: AppKeys.typesPassages[5]?['titres'] as String? ?? 'Lots',
selected: _showLots,
color: Color(AppKeys.typesPassages[5]?['couleur2'] as int),
onSelected: (selected) {
setState(() {
_showLots = selected;
_loadPassages(); // Recharger les passages avec le nouveau filtre
_saveSettings(); // Sauvegarder les préférences
});
},
),
// Filtre pour les maisons vides
_buildFilterChip(
label: AppKeys.typesPassages[6]?['titres'] as String? ??
'Maisons vides',
selected: _showMaisonsVides,
color: Color(AppKeys.typesPassages[6]?['couleur2'] as int),
onSelected: (selected) {
setState(() {
_showMaisonsVides = selected;
_loadPassages(); // Recharger les passages avec le nouveau filtre
_saveSettings(); // Sauvegarder les préférences
});
},
),
],
),
],
),
);
}
// Construire un chip de filtre
Widget _buildFilterChip({
required String label,
required bool selected,
// Construire une pastille de filtre pour la carte
Widget _buildFilterDot({
required Color color,
required Function(bool) onSelected,
required bool selected,
required VoidCallback onTap,
}) {
// Utiliser la couleur vive pour les boutons sélectionnés et une version plus terne pour les désélectionnés
final Color avatarColor = selected ? color : color.withOpacity(0.4);
final Color chipColor =
selected ? color.withOpacity(0.2) : Colors.grey.withOpacity(0.1);
return FilterChip(
label: Text(
label,
style: TextStyle(
fontWeight: selected ? FontWeight.bold : FontWeight.normal,
color: selected ? Colors.black : Colors.black54,
return GestureDetector(
onTap: onTap,
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: selected ? color : color.withValues(alpha: 0.3),
shape: BoxShape.circle,
border: Border.all(
color: selected ? Colors.white : Colors.white.withValues(alpha: 0.5),
width: 1.5,
),
),
),
selected: selected,
showCheckmark: false,
avatar: CircleAvatar(
backgroundColor: avatarColor,
radius: 10.0,
),
backgroundColor: Colors.white,
selectedColor: chipColor,
side: BorderSide(
color: selected ? color : Colors.grey.withOpacity(0.3),
width: selected ? 1.5 : 1.0,
),
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
onSelected: onSelected,
);
}
@@ -864,7 +839,7 @@ class _UserMapPageState extends State<UserMapPage> {
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 6,
offset: const Offset(0, 3),
),
@@ -918,8 +893,8 @@ class _UserMapPageState extends State<UserMapPage> {
return _sectors.map((sector) {
return Polygon(
points: sector['points'] as List<LatLng>,
color: (sector['color'] as Color).withOpacity(0.3),
borderColor: (sector['color'] as Color).withOpacity(1.0),
color: (sector['color'] as Color).withValues(alpha: 0.3),
borderColor: (sector['color'] as Color).withValues(alpha: 1.0),
borderStrokeWidth: 2.0,
);
}).toList();

View File

@@ -31,15 +31,6 @@ class _UserStatisticsPageState extends State<UserStatisticsPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Statistiques',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
const SizedBox(height: 16),
// Filtres
_buildFilters(theme, isDesktop),

View File

@@ -66,7 +66,6 @@ class _AmicaleFormState extends State<AmicaleForm> {
// Pour l'upload du logo
final ImagePicker _picker = ImagePicker();
XFile? _selectedImage;
String? _logoUrl;
// Pour Stripe Connect
StripeConnectService? _stripeService;
@@ -194,6 +193,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
if (confirm != true) return;
// Afficher le loading
if (!context.mounted) return;
showDialog(
context: context,
barrierDismissible: false,
@@ -614,7 +614,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
@@ -638,7 +638,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
onTap: _selectImage,
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
color: Colors.black.withValues(alpha: 0.3),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -818,7 +818,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
@@ -1230,10 +1230,10 @@ class _AmicaleFormState extends State<AmicaleForm> {
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _stripeStatus?.statusColor.withOpacity(0.1) ?? Colors.orange.withOpacity(0.1),
color: _stripeStatus?.statusColor.withValues(alpha: 0.1) ?? Colors.orange.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _stripeStatus?.statusColor.withOpacity(0.3) ?? Colors.orange.withOpacity(0.3),
color: _stripeStatus?.statusColor.withValues(alpha: 0.3) ?? Colors.orange.withValues(alpha: 0.3),
),
),
child: Row(

View File

@@ -38,7 +38,7 @@ class AmicaleRowWidget extends StatelessWidget {
: theme.textTheme.bodyMedium;
// Couleur de fond en fonction du type de ligne
final backgroundColor = isHeader ? theme.colorScheme.primary.withOpacity(0.1) : (isAlternate ? theme.colorScheme.surface : theme.colorScheme.surface);
final backgroundColor = isHeader ? theme.colorScheme.primary.withValues(alpha: 0.1) : (isAlternate ? theme.colorScheme.surface : theme.colorScheme.surface);
return InkWell(
onTap: isHeader || onTap == null ? null : () => onTap!(amicale),
@@ -47,7 +47,7 @@ class AmicaleRowWidget extends StatelessWidget {
color: backgroundColor,
border: Border(
bottom: BorderSide(
color: theme.dividerColor.withOpacity(0.3),
color: theme.dividerColor.withValues(alpha: 0.3),
width: 1,
),
),

View File

@@ -89,7 +89,9 @@ class AmicaleTableWidget extends StatelessWidget {
await amicaleRepository.saveAmicale(updatedAmicale);
debugPrint('✅ Amicale sauvegardée dans le repository');
Navigator.of(dialogContext).pop();
if (dialogContext.mounted) {
Navigator.of(dialogContext).pop();
}
},
),
),
@@ -132,7 +134,7 @@ class AmicaleTableWidget extends StatelessWidget {
bottomRight: Radius.circular(8),
),
border: Border.all(
color: theme.colorScheme.primary.withOpacity(0.1),
color: theme.colorScheme.primary.withValues(alpha: 0.1),
width: 1,
),
),
@@ -159,7 +161,7 @@ class AmicaleTableWidget extends StatelessWidget {
child: Text(
emptyMessage ?? 'Aucune amicale trouvée',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
),
),
@@ -237,7 +239,9 @@ class AmicaleTableWidget extends StatelessWidget {
await amicaleRepository.saveAmicale(updatedAmicale);
debugPrint('✅ Amicale sauvegardée dans le repository');
Navigator.of(dialogContext).pop();
if (dialogContext.mounted) {
Navigator.of(dialogContext).pop();
}
},
),
],

View File

@@ -146,7 +146,7 @@ class CombinedChart extends StatelessWidget {
child: Text(
formattedDate,
style: TextStyle(
color: theme.colorScheme.onSurface.withOpacity(0.6),
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
fontSize: 10,
),
),
@@ -166,7 +166,7 @@ class CombinedChart extends StatelessWidget {
child: Text(
value.toInt().toString(),
style: TextStyle(
color: theme.colorScheme.onSurface.withOpacity(0.6),
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
fontSize: 10,
),
),
@@ -189,7 +189,7 @@ class CombinedChart extends StatelessWidget {
child: Text(
'${amountValue.toInt()}',
style: TextStyle(
color: theme.colorScheme.onSurface.withOpacity(0.6),
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
fontSize: 10,
),
),
@@ -206,7 +206,7 @@ class CombinedChart extends StatelessWidget {
show: true,
getDrawingHorizontalLine: (value) {
return FlLine(
color: theme.dividerColor.withOpacity(0.2),
color: theme.dividerColor.withValues(alpha: 0.2),
strokeWidth: 1,
);
},
@@ -220,7 +220,7 @@ class CombinedChart extends StatelessWidget {
extraLinesOnTop: true,
),
),
swapAnimationDuration: const Duration(milliseconds: 250),
duration: const Duration(milliseconds: 250),
),
);
}

View File

@@ -90,7 +90,7 @@ class PassageSummaryCard extends StatelessWidget {
backgroundIcon,
size: backgroundIconSize,
color: (backgroundIconColor ?? AppTheme.primaryColor)
.withOpacity(backgroundIconOpacity),
.withValues(alpha: backgroundIconOpacity),
),
),
),
@@ -104,7 +104,7 @@ class PassageSummaryCard extends StatelessWidget {
// Titre avec comptage
useValueListenable
? _buildTitleWithValueListenable()
: _buildTitleWithStaticData(),
: _buildTitleWithStaticData(context),
const Divider(height: 24),
// Contenu principal
Expanded(
@@ -117,7 +117,7 @@ class PassageSummaryCard extends StatelessWidget {
flex: isDesktop ? 1 : 2,
child: useValueListenable
? _buildPassagesListWithValueListenable()
: _buildPassagesListWithStaticData(),
: _buildPassagesListWithStaticData(context),
),
// Séparateur vertical
@@ -176,8 +176,8 @@ class PassageSummaryCard extends StatelessWidget {
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 16,
style: TextStyle(
fontSize: AppTheme.r(context, 16),
fontWeight: FontWeight.bold,
),
),
@@ -186,7 +186,7 @@ class PassageSummaryCard extends StatelessWidget {
customTotalDisplay?.call(totalUserPassages) ??
totalUserPassages.toString(),
style: TextStyle(
fontSize: 20,
fontSize: AppTheme.r(context, 20),
fontWeight: FontWeight.bold,
color: titleColor,
),
@@ -198,7 +198,7 @@ class PassageSummaryCard extends StatelessWidget {
}
/// Construction du titre avec données statiques
Widget _buildTitleWithStaticData() {
Widget _buildTitleWithStaticData(BuildContext context) {
final totalPassages =
passagesByType?.values.fold(0, (sum, count) => sum + count) ?? 0;
@@ -215,8 +215,8 @@ class PassageSummaryCard extends StatelessWidget {
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 16,
style: TextStyle(
fontSize: AppTheme.r(context, 16),
fontWeight: FontWeight.bold,
),
),
@@ -224,7 +224,7 @@ class PassageSummaryCard extends StatelessWidget {
Text(
customTotalDisplay?.call(totalPassages) ?? totalPassages.toString(),
style: TextStyle(
fontSize: 20,
fontSize: AppTheme.r(context, 20),
fontWeight: FontWeight.bold,
color: titleColor,
),
@@ -241,18 +241,18 @@ class PassageSummaryCard extends StatelessWidget {
builder: (context, Box<PassageModel> passagesBox, child) {
final passagesCounts = _calculatePassagesCounts(passagesBox);
return _buildPassagesList(passagesCounts);
return _buildPassagesList(context, passagesCounts);
},
);
}
/// Construction de la liste des passages avec données statiques
Widget _buildPassagesListWithStaticData() {
return _buildPassagesList(passagesByType ?? {});
Widget _buildPassagesListWithStaticData(BuildContext context) {
return _buildPassagesList(context, passagesByType ?? {});
}
/// Construction de la liste des passages
Widget _buildPassagesList(Map<int, int> passagesCounts) {
Widget _buildPassagesList(BuildContext context, Map<int, int> passagesCounts) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -284,13 +284,13 @@ class PassageSummaryCard extends StatelessWidget {
Expanded(
child: Text(
typeData['titres'] as String,
style: const TextStyle(fontSize: 14),
style: TextStyle(fontSize: AppTheme.r(context, 14)),
),
),
Text(
count.toString(),
style: TextStyle(
fontSize: 16,
fontSize: AppTheme.r(context, 16),
fontWeight: FontWeight.bold,
color: color,
),

View File

@@ -87,7 +87,7 @@ class PaymentSummaryCard extends StatelessWidget {
backgroundIcon,
size: backgroundIconSize,
color: (backgroundIconColor ?? Colors.blue)
.withOpacity(backgroundIconOpacity),
.withValues(alpha: backgroundIconOpacity),
),
),
),
@@ -101,7 +101,7 @@ class PaymentSummaryCard extends StatelessWidget {
// Titre avec comptage
useValueListenable
? _buildTitleWithValueListenable()
: _buildTitleWithStaticData(),
: _buildTitleWithStaticData(context),
const Divider(height: 24),
// Contenu principal
Expanded(
@@ -114,7 +114,7 @@ class PaymentSummaryCard extends StatelessWidget {
flex: isDesktop ? 1 : 2,
child: useValueListenable
? _buildPaymentsListWithValueListenable()
: _buildPaymentsListWithStaticData(),
: _buildPaymentsListWithStaticData(context),
),
// Séparateur vertical
@@ -179,8 +179,8 @@ class PaymentSummaryCard extends StatelessWidget {
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 16,
style: TextStyle(
fontSize: AppTheme.r(context, 16),
fontWeight: FontWeight.bold,
),
),
@@ -189,7 +189,7 @@ class PaymentSummaryCard extends StatelessWidget {
customTotalDisplay?.call(paymentStats['totalAmount']) ??
'${paymentStats['totalAmount'].toStringAsFixed(2)}',
style: TextStyle(
fontSize: 20,
fontSize: AppTheme.r(context, 20),
fontWeight: FontWeight.bold,
color: titleColor,
),
@@ -201,7 +201,7 @@ class PaymentSummaryCard extends StatelessWidget {
}
/// Construction du titre avec données statiques
Widget _buildTitleWithStaticData() {
Widget _buildTitleWithStaticData(BuildContext context) {
final totalAmount =
paymentsByType?.values.fold(0.0, (sum, amount) => sum + amount) ?? 0.0;
@@ -218,8 +218,8 @@ class PaymentSummaryCard extends StatelessWidget {
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 16,
style: TextStyle(
fontSize: AppTheme.r(context, 16),
fontWeight: FontWeight.bold,
),
),
@@ -228,7 +228,7 @@ class PaymentSummaryCard extends StatelessWidget {
customTotalDisplay?.call(totalAmount) ??
'${totalAmount.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 20,
fontSize: AppTheme.r(context, 20),
fontWeight: FontWeight.bold,
color: titleColor,
),
@@ -245,18 +245,18 @@ class PaymentSummaryCard extends StatelessWidget {
builder: (context, Box<PassageModel> passagesBox, child) {
final paymentAmounts = _calculatePaymentAmounts(passagesBox);
return _buildPaymentsList(paymentAmounts);
return _buildPaymentsList(context, paymentAmounts);
},
);
}
/// Construction de la liste des règlements avec données statiques
Widget _buildPaymentsListWithStaticData() {
return _buildPaymentsList(paymentsByType ?? {});
Widget _buildPaymentsListWithStaticData(BuildContext context) {
return _buildPaymentsList(context, paymentsByType ?? {});
}
/// Construction de la liste des règlements
Widget _buildPaymentsList(Map<int, double> paymentAmounts) {
Widget _buildPaymentsList(BuildContext context, Map<int, double> paymentAmounts) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -288,13 +288,13 @@ class PaymentSummaryCard extends StatelessWidget {
Expanded(
child: Text(
typeData['titre'] as String,
style: const TextStyle(fontSize: 14),
style: TextStyle(fontSize: AppTheme.r(context, 14)),
),
),
Text(
'${amount.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 16,
fontSize: AppTheme.r(context, 16),
fontWeight: FontWeight.bold,
color: color,
),

View File

@@ -35,7 +35,7 @@ class _ChatInputState extends State<ChatInput> {
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 5,
offset: const Offset(0, -2),
),
@@ -195,7 +195,7 @@ class _ChatInputState extends State<ChatInput> {
width: 56,
height: 56,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
color: color.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(

View File

@@ -87,7 +87,7 @@ class ChatMessages extends StatelessWidget {
CircleAvatar(
radius: 16,
backgroundColor:
AppTheme.primaryColor.withOpacity(0.2),
AppTheme.primaryColor.withValues(alpha: 0.2),
backgroundImage: message['avatar'] != null
? AssetImage(message['avatar'] as String)
: null,
@@ -141,7 +141,7 @@ class ChatMessages extends StatelessWidget {
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 3,
offset: const Offset(0, 1),
),

View File

@@ -31,7 +31,7 @@ class ChatSidebar extends StatelessWidget {
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 5,
offset: const Offset(0, 2),
),
@@ -114,9 +114,9 @@ class ChatSidebar extends StatelessWidget {
return ListTile(
selected: isSelected,
selectedTileColor: Colors.blue.withOpacity(0.1),
selectedTileColor: Colors.blue.withValues(alpha: 0.1),
leading: CircleAvatar(
backgroundColor: AppTheme.primaryColor.withOpacity(0.2),
backgroundColor: AppTheme.primaryColor.withValues(alpha: 0.2),
backgroundImage: contact['avatar'] != null
? AssetImage(contact['avatar'] as String)
: null,

View File

@@ -78,7 +78,7 @@ class ClearCacheDialog extends StatelessWidget {
'Note : Cette opération est nécessaire en raison d\'une mise à jour de la structure des données. Toutes vos données seront récupérées depuis le serveur après reconnexion.',
style: theme.textTheme.bodySmall?.copyWith(
fontStyle: FontStyle.italic,
color: theme.colorScheme.onSurface.withOpacity(0.7),
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
),
],

View File

@@ -105,10 +105,10 @@ class _ConnectivityIndicatorState extends State<ConnectivityIndicator>
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: theme.colorScheme.error.withOpacity(0.1),
color: theme.colorScheme.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: theme.colorScheme.error.withOpacity(0.3),
color: theme.colorScheme.error.withValues(alpha: 0.3),
),
),
child: Row(
@@ -191,13 +191,13 @@ class _ConnectivityIndicatorState extends State<ConnectivityIndicator>
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(
color: pendingCount > 0
? Colors.orange.withOpacity(0.1 * _animation.value)
: color.withOpacity(0.1),
? Colors.orange.withValues(alpha: 0.1 * _animation.value)
: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: pendingCount > 0
? Colors.orange.withOpacity(0.3 * _animation.value)
: color.withOpacity(0.3),
? Colors.orange.withValues(alpha: 0.3 * _animation.value)
: color.withValues(alpha: 0.3),
),
),
child: Row(
@@ -238,10 +238,10 @@ class _ConnectivityIndicatorState extends State<ConnectivityIndicator>
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: theme.colorScheme.error.withOpacity(0.1),
color: theme.colorScheme.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: theme.colorScheme.error.withOpacity(0.3),
color: theme.colorScheme.error.withValues(alpha: 0.3),
),
),
child: Row(
@@ -270,10 +270,10 @@ class _ConnectivityIndicatorState extends State<ConnectivityIndicator>
return Container(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: color.withOpacity(0.3),
color: color.withValues(alpha: 0.3),
),
),
child: Row(

View File

@@ -95,7 +95,7 @@ class CustomTextField extends StatelessWidget {
child: Text(
'$currentLength/${maxLength ?? 0}',
style: theme.textTheme.bodySmall?.copyWith(
color: currentLength > (maxLength ?? 0) * 0.8 ? theme.colorScheme.error : theme.colorScheme.onSurface.withOpacity(0.6),
color: currentLength > (maxLength ?? 0) * 0.8 ? theme.colorScheme.error : theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
);
@@ -165,7 +165,7 @@ class CustomTextField extends StatelessWidget {
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: theme.colorScheme.outline.withOpacity(0.5),
color: theme.colorScheme.outline.withValues(alpha: 0.5),
),
),
focusedBorder: OutlineInputBorder(
@@ -190,7 +190,7 @@ class CustomTextField extends StatelessWidget {
),
),
filled: true,
fillColor: readOnly ? theme.colorScheme.surfaceContainerHighest.withOpacity(0.3) : theme.colorScheme.surface,
fillColor: readOnly ? theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.3) : theme.colorScheme.surface,
contentPadding: contentPadding ?? const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
@@ -203,7 +203,7 @@ class CustomTextField extends StatelessWidget {
child: Text(
'$currentLength/${maxLength ?? 0}',
style: theme.textTheme.bodySmall?.copyWith(
color: currentLength > (maxLength ?? 0) * 0.8 ? theme.colorScheme.error : theme.colorScheme.onSurface.withOpacity(0.6),
color: currentLength > (maxLength ?? 0) * 0.8 ? theme.colorScheme.error : theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
);

View File

@@ -1,12 +1,12 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:geosector_app/app.dart';
import 'package:geosector_app/core/services/app_info_service.dart';
import 'package:geosector_app/core/services/current_amicale_service.dart';
import 'package:geosector_app/presentation/widgets/connectivity_indicator.dart';
import 'package:geosector_app/presentation/widgets/user_form_dialog.dart';
import 'package:geosector_app/core/utils/api_exception.dart';
import 'package:geosector_app/core/services/theme_service.dart';
import 'package:go_router/go_router.dart';
/// AppBar personnalisée pour les tableaux de bord
@@ -36,8 +36,9 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
final theme = Theme.of(context);
// Vérifier si le logo de l'amicale est présent pour ajuster la largeur du leading
final amicale = CurrentAmicaleService.instance.currentAmicale;
final hasAmicaleLogo = amicale?.logoBase64 != null && amicale!.logoBase64!.isNotEmpty;
final hasAmicaleLogo =
amicale?.logoBase64 != null && amicale!.logoBase64!.isNotEmpty;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -48,7 +49,9 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
elevation: 4,
leading: _buildLogo(),
// Ajuster la largeur du leading si le logo de l'amicale est présent
leadingWidth: hasAmicaleLogo ? 110 : 56, // 56 par défaut, 110 pour 2 logos + espacement
leadingWidth: hasAmicaleLogo
? 110
: 56, // 56 par défaut, 110 pour 2 logos + espacement
actions: _buildActions(context),
),
// Bordure colorée selon le rôle
@@ -64,7 +67,7 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
Widget _buildLogo() {
final amicale = CurrentAmicaleService.instance.currentAmicale;
final logoBase64 = amicale?.logoBase64;
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
@@ -93,9 +96,9 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
if (logoBase64.contains('base64,')) {
base64String = logoBase64.split('base64,').last;
}
final decodedBytes = base64Decode(base64String);
return Container(
width: 40,
height: 40,
@@ -147,8 +150,8 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
actions.add(
Text(
"v${AppInfoService.version}",
style: const TextStyle(
fontSize: 12,
style: TextStyle(
fontSize: AppTheme.r(context, 12),
color: Colors.white70,
),
),
@@ -192,7 +195,9 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
}
} catch (e) {
debugPrint('❌ Erreur mise à jour de votre profil: $e');
ApiException.showError(context, e);
if (context.mounted) {
ApiException.showError(context, e);
}
}
},
),
@@ -247,8 +252,10 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
const Duration(milliseconds: 100));
// Navigation vers splash avec paramètres pour redirection automatique
final loginType = isAdmin ? 'admin' : 'user';
context.go('/?action=login&type=$loginType');
if (context.mounted) {
final loginType = isAdmin ? 'admin' : 'user';
context.go('/?action=login&type=$loginType');
}
}
},
child: const Text('Déconnexion'),
@@ -277,14 +284,15 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
builder: (context, constraints) {
// Déterminer si on est sur mobile ou écran étroit
final isNarrowScreen = constraints.maxWidth < 600;
final isMobilePlatform = Theme.of(context).platform == TargetPlatform.android ||
Theme.of(context).platform == TargetPlatform.iOS;
final isMobilePlatform =
Theme.of(context).platform == TargetPlatform.android ||
Theme.of(context).platform == TargetPlatform.iOS;
// Sur mobile ou écrans étroits, afficher seulement le titre principal
if (isNarrowScreen || isMobilePlatform) {
return Text(title);
}
// Sur écrans larges (web desktop), afficher le titre de la page ou le titre principal
// Pour les admins, on affiche directement le titre de la page sans préfixe
return Text(pageTitle!);
@@ -292,136 +300,6 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
);
}
/// Construction du sélecteur de thème avec confirmation
Widget _buildThemeSwitcherWithConfirmation(BuildContext context) {
return IconButton(
icon: Icon(ThemeService.instance.themeModeIcon),
tooltip:
'Changer le thème (${ThemeService.instance.themeModeDescription})',
onPressed: () async {
final themeService = ThemeService.instance;
final currentTheme = themeService.themeModeDescription;
// Déterminer le prochain thème
String nextTheme;
switch (themeService.themeMode) {
case ThemeMode.light:
nextTheme = 'Sombre';
break;
case ThemeMode.dark:
nextTheme = 'Clair';
break;
case ThemeMode.system:
nextTheme = themeService.isSystemDark ? 'Clair' : 'Sombre';
break;
}
// Afficher la confirmation
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Row(
children: [
Icon(Icons.palette_outlined),
SizedBox(width: 8),
Text('Changement de thème'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Vous êtes actuellement sur le thème :'),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
themeService.themeModeIcon,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
currentTheme,
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
const SizedBox(height: 16),
Text('Voulez-vous passer au thème $nextTheme ?'),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.errorContainer
.withOpacity(0.3),
borderRadius: BorderRadius.circular(6),
),
child: const Row(
children: [
Icon(Icons.warning_amber, size: 16),
SizedBox(width: 8),
Expanded(
child: Text(
'Note: Vous devrez vous reconnecter après ce changement.',
style: TextStyle(fontSize: 12),
),
),
],
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: Text('Passer au thème $nextTheme'),
),
],
),
);
// Si confirmé, changer le thème
if (confirmed == true) {
await themeService.toggleTheme();
// Déconnecter l'utilisateur
if (context.mounted) {
final success = await userRepository.logout(context);
if (success && context.mounted) {
await Future.delayed(const Duration(milliseconds: 100));
// Rediriger vers splash avec paramètres pour revenir au même type de login
final loginType = isAdmin ? 'admin' : 'user';
context.go('/?action=login&type=$loginType');
}
}
}
},
);
}
@override
Size get preferredSize =>
const Size.fromHeight(kToolbarHeight + 3); // +3 pour la bordure

View File

@@ -77,12 +77,15 @@ class DashboardLayout extends StatelessWidget {
// Déterminer le rôle de l'utilisateur
final currentUser = userRepository.getCurrentUser();
final userRole = currentUser?.role ?? 1;
// Définir les couleurs du gradient selon le rôle
final gradientColors = userRole > 1
? [Colors.white, Colors.red.shade300] // Admin : fond rouge
: [Colors.white, AppTheme.accentColor.withOpacity(0.3)]; // User : fond vert
? [Colors.white, Colors.red.shade300] // Admin : fond rouge
: [
Colors.white,
AppTheme.accentColor.withValues(alpha: 0.3)
]; // User : fond vert
return Stack(
children: [
// Fond dégradé avec points
@@ -140,9 +143,11 @@ class DashboardLayout extends StatelessWidget {
children: [
const Icon(Icons.error_outline, color: Colors.red, size: 64),
const SizedBox(height: 16),
const Text(
Text(
'Une erreur est survenue',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: AppTheme.r(context, 20),
fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text('Détails: $e'),
@@ -167,12 +172,12 @@ class DotsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(0.5)
..color = Colors.white.withValues(alpha: 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;
@@ -180,7 +185,7 @@ class DotsPainter extends CustomPainter {
canvas.drawCircle(Offset(x, y), radius, paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@@ -87,7 +87,7 @@ class HiveResetDialog extends StatelessWidget {
'Note : Si vous aviez des modifications non synchronisées, elles ont été perdues. Nous vous recommandons de synchroniser régulièrement vos données.',
style: theme.textTheme.bodySmall?.copyWith(
fontStyle: FontStyle.italic,
color: theme.colorScheme.onSurface.withOpacity(0.7),
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
),
],

View File

@@ -32,7 +32,6 @@ class _LoadingSpinOverlayState extends State<LoadingSpinOverlay>
late AnimationController _fadeController;
late AnimationController _rotationController;
late Animation<double> _fadeAnimation;
late Animation<double> _rotationAnimation;
@override
void initState() {
@@ -54,13 +53,6 @@ class _LoadingSpinOverlayState extends State<LoadingSpinOverlay>
curve: Curves.easeInOut,
));
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 2 * 3.14159,
).animate(CurvedAnimation(
parent: _rotationController,
curve: Curves.linear,
));
_fadeController.forward();
_rotationController.repeat();
@@ -103,11 +95,11 @@ class _LoadingSpinOverlayState extends State<LoadingSpinOverlay>
maxWidth: 280,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.92), // Semi-transparent
color: Colors.white.withValues(alpha: 0.92), // Semi-transparent
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 20,
spreadRadius: 2,
offset: const Offset(0, 8),

View File

@@ -26,7 +26,9 @@ class MembreRowWidget extends StatelessWidget {
final theme = Theme.of(context);
// Couleur de fond alternée
final backgroundColor = isAlternate ? theme.colorScheme.primary.withValues(alpha: 0.05) : Colors.transparent;
final backgroundColor = isAlternate
? theme.colorScheme.primary.withValues(alpha: 0.05)
: Colors.transparent;
return InkWell(
// Envelopper le contenu dans un InkWell
@@ -44,7 +46,7 @@ class MembreRowWidget extends StatelessWidget {
Expanded(
flex: 1,
child: Text(
membre.id.toString() ?? '',
membre.id.toString(),
style: theme.textTheme.bodyMedium,
),
),
@@ -82,7 +84,7 @@ class MembreRowWidget extends StatelessWidget {
Expanded(
flex: 3,
child: Text(
membre.email ?? '',
membre.email,
style: theme.textTheme.bodyMedium,
),
),
@@ -143,66 +145,6 @@ class MembreRowWidget extends StatelessWidget {
);
}
// Afficher les détails du membre dans une boîte de dialogue
void _showMembreDetails(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('${membre.firstName ?? ''} ${membre.name ?? ''}'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow('ID', membre.id.toString()),
_buildDetailRow('Email', membre.email),
_buildDetailRow('Username', membre.username ?? 'Non défini'),
_buildDetailRow('Rôle', _getRoleName(membre.role)),
_buildDetailRow('Titre', membre.fkTitre?.toString() ?? 'Non défini'),
_buildDetailRow('Secteur', membre.sectName ?? 'Non défini'),
_buildDetailRow('Statut', membre.isActive ? 'Actif' : 'Inactif'),
_buildDetailRow('Téléphone', membre.phone ?? 'Non défini'),
_buildDetailRow('Mobile', membre.mobile ?? 'Non défini'),
if (membre.dateNaissance != null)
_buildDetailRow('Date de naissance', '${membre.dateNaissance!.day}/${membre.dateNaissance!.month}/${membre.dateNaissance!.year}'),
if (membre.dateEmbauche != null)
_buildDetailRow('Date d\'embauche', '${membre.dateEmbauche!.day}/${membre.dateEmbauche!.month}/${membre.dateEmbauche!.year}'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
],
),
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
Expanded(
child: Text(value),
),
],
),
);
}
Color _getStatusColor(bool? isActive) {
return isActive == true ? Colors.green : Colors.red;
}
// Méthode pour convertir l'ID de rôle en nom lisible
String _getRoleName(int roleId) {
switch (roleId) {

View File

@@ -43,7 +43,7 @@ class MembreTableWidget extends StatelessWidget {
borderRadius: BorderRadius.circular(8.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
@@ -58,7 +58,7 @@ class MembreTableWidget extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
margin: const EdgeInsets.only(bottom: 16.0),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withOpacity(0.1),
color: theme.colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4.0),
),
child: Row(
@@ -189,7 +189,7 @@ class MembreTableWidget extends StatelessWidget {
child: Text(
emptyMessage ?? 'Aucun membre trouvé',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
),
),
);
@@ -199,7 +199,7 @@ class MembreTableWidget extends StatelessWidget {
return ListView.separated(
itemCount: membres.length,
separatorBuilder: (context, index) => Divider(
color: Theme.of(context).dividerColor.withOpacity(0.3),
color: Theme.of(context).dividerColor.withValues(alpha: 0.3),
height: 1,
),
itemBuilder: (context, index) {

View File

@@ -2,7 +2,6 @@ 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

View File

@@ -310,9 +310,9 @@ class _OperationFormDialogState extends State<OperationFormDialog> {
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: theme.colorScheme.outline.withOpacity(0.5)),
border: Border.all(color: theme.colorScheme.outline.withValues(alpha: 0.5)),
borderRadius: BorderRadius.circular(8),
color: theme.colorScheme.surface.withOpacity(0.3),
color: theme.colorScheme.surface.withValues(alpha: 0.3),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -422,10 +422,10 @@ class _OperationFormDialogState extends State<OperationFormDialog> {
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.secondaryContainer.withOpacity(0.3),
color: theme.colorScheme.secondaryContainer.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: theme.colorScheme.outline.withOpacity(0.3),
color: theme.colorScheme.outline.withValues(alpha: 0.3),
),
),
child: Row(

View File

@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/repositories/passage_repository.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
@@ -184,8 +186,10 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
// Initialiser la date de passage
_passedAt = passage?.passedAt ?? DateTime.now();
final String dateFormatted = '${_passedAt.day.toString().padLeft(2, '0')}/${_passedAt.month.toString().padLeft(2, '0')}/${_passedAt.year}';
final String timeFormatted = '${_passedAt.hour.toString().padLeft(2, '0')}:${_passedAt.minute.toString().padLeft(2, '0')}';
final String dateFormatted =
'${_passedAt.day.toString().padLeft(2, '0')}/${_passedAt.month.toString().padLeft(2, '0')}/${_passedAt.year}';
final String timeFormatted =
'${_passedAt.hour.toString().padLeft(2, '0')}:${_passedAt.minute.toString().padLeft(2, '0')}';
debugPrint('Valeurs pour controllers:');
debugPrint(' numero: "$numero"');
@@ -258,12 +262,14 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
_montantController.text = '';
_fkTypeReglement = 4; // Non renseigné
}
// Si c'est un nouveau passage et qu'on change de type, réinitialiser la date à maintenant
if (widget.passage == null) {
_passedAt = DateTime.now();
_dateController.text = '${_passedAt.day.toString().padLeft(2, '0')}/${_passedAt.month.toString().padLeft(2, '0')}/${_passedAt.year}';
_timeController.text = '${_passedAt.hour.toString().padLeft(2, '0')}:${_passedAt.minute.toString().padLeft(2, '0')}';
_dateController.text =
'${_passedAt.day.toString().padLeft(2, '0')}/${_passedAt.month.toString().padLeft(2, '0')}/${_passedAt.year}';
_timeController.text =
'${_passedAt.hour.toString().padLeft(2, '0')}:${_passedAt.minute.toString().padLeft(2, '0')}';
}
});
}
@@ -366,7 +372,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
if (success && mounted) {
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) {
Navigator.of(context).pop();
Navigator.of(context, rootNavigator: false).pop();
widget.onSuccess?.call();
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
@@ -420,7 +426,8 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: MediaQuery.of(context).size.width < 600 ? 1.8 : 2.5,
childAspectRatio:
MediaQuery.of(context).size.width < 600 ? 1.8 : 2.5,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
@@ -445,7 +452,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Color(typeData['couleur2'] as int? ?? 0xFF000000)
.withOpacity(0.15),
.withValues(alpha: 0.15),
border: Border.all(
color: Color(typeData['couleur2'] as int? ?? 0xFF000000),
width: isSelected ? 3 : 2,
@@ -456,7 +463,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
BoxShadow(
color: Color(typeData['couleur2'] as int? ??
0xFF000000)
.withOpacity(0.2),
.withValues(alpha: 0.2),
blurRadius: 8,
offset: const Offset(0, 2),
)
@@ -504,7 +511,6 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
Widget _buildPassageForm() {
try {
debugPrint('=== DEBUT _buildPassageForm ===');
final theme = Theme.of(context);
debugPrint('Building Form...');
return Form(
@@ -549,7 +555,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
],
),
const SizedBox(height: 24),
// Section Adresse
FormSection(
title: 'Adresse',
@@ -740,7 +746,9 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
// Section Règlement et Remarque
FormSection(
title: (_selectedPassageType == 1 || _selectedPassageType == 5) ? 'Règlement et Note' : 'Note',
title: (_selectedPassageType == 1 || _selectedPassageType == 5)
? 'Règlement et Note'
: 'Note',
icon: Icons.note,
children: [
// Afficher montant et type de règlement seulement pour fkType 1 (Effectué) ou 5 (Lot)
@@ -755,7 +763,8 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
showLabel: false,
hintText: "0.00",
textAlign: TextAlign.right,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
keyboardType: const TextInputType.numberWithOptions(
decimal: true),
readOnly: widget.readOnly,
validator: _validateMontant,
prefixIcon: Icons.euro,
@@ -764,7 +773,7 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
const SizedBox(width: 12),
Expanded(
child: DropdownButtonFormField<int>(
value: _fkTypeReglement,
initialValue: _fkTypeReglement,
decoration: const InputDecoration(
labelText: "Type de règlement *",
border: OutlineInputBorder(),
@@ -792,7 +801,8 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
_fkTypeReglement = value!;
});
},
validator: (_selectedPassageType == 1 || _selectedPassageType == 5)
validator: (_selectedPassageType == 1 ||
_selectedPassageType == 5)
? (value) {
if (value == null || value < 1 || value > 3) {
return 'Type de règlement requis';
@@ -837,9 +847,6 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
}
}
Future<void> _selectDate() async {
final DateTime? picked = await showDatePicker(
context: context,
@@ -856,7 +863,8 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
_passedAt.hour,
_passedAt.minute,
);
_dateController.text = '${_passedAt.day.toString().padLeft(2, '0')}/${_passedAt.month.toString().padLeft(2, '0')}/${_passedAt.year}';
_dateController.text =
'${_passedAt.day.toString().padLeft(2, '0')}/${_passedAt.month.toString().padLeft(2, '0')}/${_passedAt.year}';
});
}
}
@@ -875,160 +883,324 @@ class _PassageFormDialogState extends State<PassageFormDialog> {
picked.hour,
picked.minute,
);
_timeController.text = '${_passedAt.hour.toString().padLeft(2, '0')}:${_passedAt.minute.toString().padLeft(2, '0')}';
_timeController.text =
'${_passedAt.hour.toString().padLeft(2, '0')}:${_passedAt.minute.toString().padLeft(2, '0')}';
});
}
}
// Méthode pour détecter si on est sur mobile
bool _isMobile(BuildContext context) {
// Détecter si on est sur mobile natif ou web mobile (largeur < 600px)
return Theme.of(context).platform == TargetPlatform.iOS ||
Theme.of(context).platform == TargetPlatform.android ||
(kIsWeb && MediaQuery.of(context).size.width < 600);
}
// Méthode pour construire l'en-tête du formulaire
Widget _buildHeader() {
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: _selectedPassageType != null &&
AppKeys.typesPassages.containsKey(_selectedPassageType)
? Color(AppKeys.typesPassages[_selectedPassageType]!['couleur2']
as int? ??
0xFF000000)
.withValues(alpha: 0.1)
: null,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
Icon(
widget.passage == null ? Icons.add_circle : Icons.edit,
color: _selectedPassageType != null &&
AppKeys.typesPassages
.containsKey(_selectedPassageType)
? Color(AppKeys.typesPassages[_selectedPassageType]![
'couleur2'] as int? ??
0xFF000000)
: theme.colorScheme.primary,
),
const SizedBox(width: 8),
Flexible(
child: Text(
widget.title,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: _selectedPassageType != null &&
AppKeys.typesPassages
.containsKey(_selectedPassageType)
? Color(AppKeys.typesPassages[_selectedPassageType]![
'couleur2'] as int? ??
0xFF000000)
: theme.colorScheme.primary,
),
overflow: TextOverflow.ellipsis,
),
),
if (_selectedPassageType != null &&
AppKeys.typesPassages
.containsKey(_selectedPassageType)) ...[
const SizedBox(width: 12),
Icon(
AppKeys.typesPassages[_selectedPassageType]!['icon_data']
as IconData? ??
Icons.help,
color: Color(
AppKeys.typesPassages[_selectedPassageType]!['couleur2']
as int? ??
0xFF000000),
size: 20,
),
const SizedBox(width: 4),
Text(
AppKeys.typesPassages[_selectedPassageType]!['titre']
as String? ??
'Inconnu',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: Color(AppKeys.typesPassages[_selectedPassageType]![
'couleur2'] as int? ??
0xFF000000),
),
),
],
],
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: _isSubmitting ? null : () {
Navigator.of(context, rootNavigator: false).pop();
},
),
],
),
);
}
// Méthode pour construire le contenu principal
Widget _buildContent() {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!_showForm) ...[
() {
debugPrint('Building passage type selection...');
return _buildPassageTypeSelection();
}(),
] else ...[
() {
debugPrint('Building passage form...');
return _buildPassageForm();
}(),
],
],
),
);
}
// Méthode pour construire les boutons du footer
Widget _buildFooterButtons() {
final theme = Theme.of(context);
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: _isSubmitting ? null : () {
Navigator.of(context, rootNavigator: false).pop();
},
child: const Text('Annuler'),
),
const SizedBox(width: 16),
if (!widget.readOnly && _showForm && _selectedPassageType != null)
ElevatedButton.icon(
onPressed: _isSubmitting ? null : _handleSubmit,
icon: _isSubmitting
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(widget.passage == null ? Icons.add : Icons.save),
label: Text(_isSubmitting
? 'Enregistrement...'
: (widget.passage == null ? 'Créer' : 'Enregistrer')),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: Colors.white,
),
),
],
);
}
// Méthode pour construire le contenu du Dialog
Widget _buildDialogContent() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
_buildHeader(),
const Divider(),
// Contenu
Expanded(
child: _buildContent(),
),
const SizedBox(height: 24),
// Footer
_buildFooterButtons(),
],
);
}
// Méthode pour construire l'AppBar mobile
AppBar _buildMobileAppBar() {
final theme = Theme.of(context);
final typeColor = _selectedPassageType != null &&
AppKeys.typesPassages.containsKey(_selectedPassageType)
? Color(
AppKeys.typesPassages[_selectedPassageType]!['couleur2'] as int? ??
0xFF000000)
: theme.colorScheme.primary;
return AppBar(
backgroundColor: typeColor.withValues(alpha: 0.1),
elevation: 0,
leading: IconButton(
icon: Icon(Icons.close, color: typeColor),
onPressed: _isSubmitting ? null : () {
Navigator.of(context, rootNavigator: false).pop();
},
),
title: Row(
children: [
Icon(
widget.passage == null ? Icons.add_circle : Icons.edit,
color: typeColor,
size: 24,
),
const SizedBox(width: 8),
Expanded(
child: Text(
widget.title,
style: TextStyle(
color: typeColor,
fontWeight: FontWeight.bold,
fontSize: AppTheme.r(context, 18),
),
overflow: TextOverflow.ellipsis,
),
),
],
),
actions: _selectedPassageType != null &&
AppKeys.typesPassages.containsKey(_selectedPassageType)
? [
Padding(
padding: const EdgeInsets.only(right: 8),
child: Row(
children: [
Icon(
AppKeys.typesPassages[_selectedPassageType]!['icon_data']
as IconData? ??
Icons.help,
color: typeColor,
size: 20,
),
const SizedBox(width: 4),
Text(
AppKeys.typesPassages[_selectedPassageType]!['titre']
as String? ??
'Inconnu',
style: TextStyle(
fontWeight: FontWeight.w600,
color: typeColor,
fontSize: AppTheme.r(context, 14),
),
),
],
),
),
]
: null,
);
}
@override
Widget build(BuildContext context) {
try {
debugPrint('=== DEBUT PassageFormDialog.build ===');
final theme = Theme.of(context);
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
insetPadding: const EdgeInsets.all(24),
child: Container(
width: MediaQuery.of(context).size.width * 0.6,
constraints: const BoxConstraints(
maxWidth: 800,
maxHeight: 900,
),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
decoration: BoxDecoration(
color: _selectedPassageType != null && AppKeys.typesPassages.containsKey(_selectedPassageType)
? Color(AppKeys.typesPassages[_selectedPassageType]!['couleur2'] as int? ?? 0xFF000000).withOpacity(0.1)
: null,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
Icon(
widget.passage == null
? Icons.add_circle
: Icons.edit,
color: _selectedPassageType != null && AppKeys.typesPassages.containsKey(_selectedPassageType)
? Color(AppKeys.typesPassages[_selectedPassageType]!['couleur2'] as int? ?? 0xFF000000)
: theme.colorScheme.primary,
),
const SizedBox(width: 8),
Flexible(
child: Text(
widget.title,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: _selectedPassageType != null && AppKeys.typesPassages.containsKey(_selectedPassageType)
? Color(AppKeys.typesPassages[_selectedPassageType]!['couleur2'] as int? ?? 0xFF000000)
: theme.colorScheme.primary,
),
overflow: TextOverflow.ellipsis,
),
),
if (_selectedPassageType != null && AppKeys.typesPassages.containsKey(_selectedPassageType)) ...[
const SizedBox(width: 12),
Icon(
AppKeys.typesPassages[_selectedPassageType]!['icon_data'] as IconData? ?? Icons.help,
color: Color(AppKeys.typesPassages[_selectedPassageType]!['couleur2'] as int? ?? 0xFF000000),
size: 20,
),
const SizedBox(width: 4),
Text(
AppKeys.typesPassages[_selectedPassageType]!['titre'] as String? ?? 'Inconnu',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: Color(AppKeys.typesPassages[_selectedPassageType]!['couleur2'] as int? ?? 0xFF000000),
),
),
],
],
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: _isSubmitting
? null
: () => Navigator.of(context).pop(),
),
],
),
),
const Divider(),
final isMobile = _isMobile(context);
debugPrint('Platform mobile détectée: $isMobile');
// Contenu
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!_showForm) ...[
() {
debugPrint('Building passage type selection...');
return _buildPassageTypeSelection();
}(),
] else ...[
() {
debugPrint('Building passage form...');
return _buildPassageForm();
}(),
],
],
),
),
),
const SizedBox(height: 24),
// Footer
Row(
mainAxisAlignment: MainAxisAlignment.end,
if (isMobile) {
// Mode plein écran pour mobile
return Scaffold(
appBar: _buildMobileAppBar(),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextButton(
onPressed: _isSubmitting
? null
: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
Expanded(
child: _buildContent(),
),
const SizedBox(width: 16),
if (!widget.readOnly &&
_showForm &&
_selectedPassageType != null)
ElevatedButton.icon(
onPressed: _isSubmitting ? null : _handleSubmit,
icon: _isSubmitting
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(
widget.passage == null ? Icons.add : Icons.save),
label: Text(_isSubmitting
? 'Enregistrement...'
: (widget.passage == null ? 'Créer' : 'Enregistrer')),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: Colors.white,
),
),
],
),
],
),
),
),
);
bottomNavigationBar: _showForm && _selectedPassageType != null
? SafeArea(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: _buildFooterButtons(),
),
)
: null,
);
} else {
// Mode Dialog pour desktop/tablette
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
insetPadding: const EdgeInsets.all(24),
child: Container(
width: MediaQuery.of(context).size.width * 0.6,
constraints: const BoxConstraints(
maxWidth: 800,
maxHeight: 900,
),
padding: const EdgeInsets.all(24),
child: _buildDialogContent(),
),
);
}
} catch (e, stackTrace) {
debugPrint('=== ERREUR PassageFormDialog.build ===');
debugPrint('Erreur: $e');

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/repositories/passage_repository.dart';
import 'package:geosector_app/core/utils/api_exception.dart';
import 'package:geosector_app/core/services/current_amicale_service.dart';
import 'package:geosector_app/app.dart';
@@ -10,24 +9,27 @@ class PassageMapDialog extends StatelessWidget {
final PassageModel passage;
final bool isAdmin;
final VoidCallback? onDeleted;
const PassageMapDialog({
super.key,
required this.passage,
this.isAdmin = false,
this.onDeleted,
});
@override
Widget build(BuildContext context) {
final int type = passage.fkType;
// Récupérer le type de passage
final String typePassage = AppKeys.typesPassages[type]?['titre'] ?? 'Inconnu';
final Color typeColor = Color(AppKeys.typesPassages[type]?['couleur1'] ?? 0xFF9E9E9E);
final String typePassage =
AppKeys.typesPassages[type]?['titre'] ?? 'Inconnu';
final Color typeColor =
Color(AppKeys.typesPassages[type]?['couleur1'] ?? 0xFF9E9E9E);
// Construire l'adresse complète
final String adresse = '${passage.numero ?? ''} ${passage.rueBis ?? ''} ${passage.rue ?? ''}'.trim();
final String adresse =
'${passage.numero} ${passage.rueBis} ${passage.rue}'.trim();
// Informations sur l'étage, l'appartement et la résidence (si habitat = 2)
String? etageInfo;
@@ -49,7 +51,8 @@ class PassageMapDialog extends StatelessWidget {
String? dateInfo;
if (type != 2 && passage.passedAt != null) {
final date = passage.passedAt!;
dateInfo = '${_formatDate(date)} à ${date.hour}h${date.minute.toString().padLeft(2, '0')}';
dateInfo =
'${_formatDate(date)} à ${date.hour}h${date.minute.toString().padLeft(2, '0')}';
}
// Récupérer le nom du passage (si le type n'est pas 6 - Maison vide)
@@ -66,7 +69,8 @@ class PassageMapDialog extends StatelessWidget {
// Récupérer les informations du type de règlement
if (AppKeys.typesReglements.containsKey(typeReglementId)) {
final Map<String, dynamic> typeReglement = AppKeys.typesReglements[typeReglementId]!;
final Map<String, dynamic> typeReglement =
AppKeys.typesReglements[typeReglementId]!;
final String titre = typeReglement['titre'] as String;
final Color couleur = Color(typeReglement['couleur'] as int);
final IconData iconData = typeReglement['icon_data'] as IconData;
@@ -75,22 +79,23 @@ class PassageMapDialog extends StatelessWidget {
padding: const EdgeInsets.all(8),
margin: const EdgeInsets.only(top: 8),
decoration: BoxDecoration(
color: couleur.withOpacity(0.1),
color: couleur.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: couleur.withOpacity(0.3)),
border: Border.all(color: couleur.withValues(alpha: 0.3)),
),
child: Row(
children: [
Icon(iconData, color: couleur, size: 20),
const SizedBox(width: 8),
Text('$titre: $montant',
style: TextStyle(color: couleur, fontWeight: FontWeight.bold)),
Text('$titre: $montant',
style:
TextStyle(color: couleur, fontWeight: FontWeight.bold)),
],
),
);
}
}
// Vérifier si l'utilisateur peut supprimer (admin ou user avec permission)
bool canDelete = isAdmin;
if (!isAdmin) {
@@ -125,7 +130,7 @@ class PassageMapDialog extends StatelessWidget {
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: typeColor.withOpacity(0.2),
color: typeColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
@@ -150,7 +155,7 @@ class PassageMapDialog extends StatelessWidget {
padding: const EdgeInsets.all(8),
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
color: Colors.red.withValues(alpha: 0.1),
border: Border.all(color: Colors.red, width: 1),
borderRadius: BorderRadius.circular(4),
),
@@ -161,42 +166,43 @@ class PassageMapDialog extends StatelessWidget {
const Expanded(
child: Text(
'Ce passage n\'est plus affecté à un secteur',
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
style: TextStyle(
color: Colors.red, fontWeight: FontWeight.bold),
),
),
],
),
),
],
// Adresse
_buildInfoRow(Icons.location_on, 'Adresse', adresse.isEmpty ? 'Non renseignée' : adresse),
_buildInfoRow(Icons.location_on, 'Adresse',
adresse.isEmpty ? 'Non renseignée' : adresse),
// Résidence
if (residenceInfo != null)
_buildInfoRow(Icons.apartment, 'Résidence', residenceInfo),
// Étage et appartement
if (etageInfo != null || apptInfo != null)
_buildInfoRow(Icons.stairs, 'Localisation',
[etageInfo, apptInfo].where((e) => e != null).join(' - ')),
_buildInfoRow(Icons.stairs, 'Localisation',
[etageInfo, apptInfo].where((e) => e != null).join(' - ')),
// Date
if (dateInfo != null)
_buildInfoRow(Icons.calendar_today, 'Date', dateInfo),
// Nom
if (nomInfo != null)
_buildInfoRow(Icons.person, 'Nom', nomInfo),
if (nomInfo != null) _buildInfoRow(Icons.person, 'Nom', nomInfo),
// Ville
if (passage.ville.isNotEmpty)
_buildInfoRow(Icons.location_city, 'Ville', passage.ville),
// Remarque
if (passage.remarque.isNotEmpty)
_buildInfoRow(Icons.note, 'Remarque', passage.remarque),
// Règlement
if (reglementInfo != null) reglementInfo,
],
@@ -224,7 +230,7 @@ class PassageMapDialog extends StatelessWidget {
],
);
}
// Helper pour construire une ligne d'information
Widget _buildInfoRow(IconData icon, String label, String value) {
return Padding(
@@ -252,18 +258,19 @@ class PassageMapDialog extends StatelessWidget {
),
);
}
// Formater une date
String _formatDate(DateTime date) {
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
}
// Afficher le dialog de confirmation de suppression
void _showDeleteConfirmationDialog(BuildContext context) {
final TextEditingController confirmController = TextEditingController();
final String streetNumber = passage.numero ?? '';
final String fullAddress = '${passage.numero ?? ''} ${passage.rueBis ?? ''} ${passage.rue ?? ''}'.trim();
final String streetNumber = passage.numero;
final String fullAddress =
'${passage.numero} ${passage.rueBis} ${passage.rue}'.trim();
showDialog(
context: context,
barrierDismissible: false,
@@ -335,7 +342,9 @@ class PassageMapDialog extends StatelessWidget {
controller: confirmController,
decoration: InputDecoration(
labelText: 'Numéro de rue',
hintText: streetNumber.isNotEmpty ? 'Ex: $streetNumber' : 'Saisir le numéro',
hintText: streetNumber.isNotEmpty
? 'Ex: $streetNumber'
: 'Saisir le numéro',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.home),
),
@@ -366,8 +375,9 @@ class PassageMapDialog extends StatelessWidget {
);
return;
}
if (streetNumber.isNotEmpty && enteredNumber.toUpperCase() != streetNumber.toUpperCase()) {
if (streetNumber.isNotEmpty &&
enteredNumber.toUpperCase() != streetNumber.toUpperCase()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Le numéro de rue ne correspond pas'),
@@ -376,11 +386,11 @@ class PassageMapDialog extends StatelessWidget {
);
return;
}
// Fermer le dialog
confirmController.dispose();
Navigator.of(dialogContext).pop();
// Effectuer la suppression
await _deletePassage(context);
},
@@ -395,20 +405,21 @@ class PassageMapDialog extends StatelessWidget {
},
);
}
// Supprimer un passage
Future<void> _deletePassage(BuildContext context) async {
try {
// Appeler le repository pour supprimer via l'API
final success = await passageRepository.deletePassageViaApi(passage.id);
if (success && context.mounted) {
ApiException.showSuccess(context, 'Passage supprimé avec succès');
// Appeler le callback si fourni
onDeleted?.call();
} else if (context.mounted) {
ApiException.showError(context, Exception('Erreur lors de la suppression'));
ApiException.showError(
context, Exception('Erreur lors de la suppression'));
}
} catch (e) {
debugPrint('Erreur suppression passage: $e');
@@ -417,4 +428,4 @@ class PassageMapDialog extends StatelessWidget {
}
}
}
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import '../custom_text_field.dart';
class PassageForm extends StatefulWidget {
@@ -217,21 +218,21 @@ class _PassageFormState extends State<PassageForm> {
decoration: InputDecoration(
hintText: '0.00 €',
hintStyle: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.5),
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
fillColor: const Color(0xFFF4F5F6),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: theme.colorScheme.onSurface.withOpacity(0.1),
color: theme.colorScheme.onSurface.withValues(alpha: 0.1),
width: 1,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: theme.colorScheme.onSurface.withOpacity(0.1),
color: theme.colorScheme.onSurface.withValues(alpha: 0.1),
width: 1,
),
),
@@ -312,10 +313,10 @@ class _PassageFormState extends State<PassageForm> {
),
minimumSize: const Size(200, 50),
),
child: const Text(
child: Text(
'Enregistrer',
style: TextStyle(
fontSize: 18,
fontSize: AppTheme.r(context, 18),
fontWeight: FontWeight.w500,
),
),
@@ -332,7 +333,6 @@ class _PassageFormState extends State<PassageForm> {
required Function(String?) onChanged,
}) {
final theme = Theme.of(context);
final isSelected = value == groupValue;
return Row(
children: [
@@ -359,10 +359,10 @@ class _PassageFormState extends State<PassageForm> {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5),
decoration: BoxDecoration(
color: const Color(0xFFF4F5F6).withOpacity(0.85),
color: const Color(0xFFF4F5F6).withValues(alpha: 0.85),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFF20335E).withOpacity(0.1),
color: const Color(0xFF20335E).withValues(alpha: 0.1),
width: 1,
),
),

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:geosector_app/core/services/current_amicale_service.dart';
import 'package:geosector_app/core/repositories/passage_repository.dart';
import 'package:geosector_app/core/utils/api_exception.dart';
import 'package:geosector_app/app.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
@@ -40,7 +40,7 @@ class PassagesListWidget extends StatefulWidget {
/// Callback appelé lorsque les détails sont demandés
final Function(Map<String, dynamic>)? onDetailsView;
/// Callback appelé lorsqu'un passage est supprimé (optionnel)
final Function(Map<String, dynamic>)? onPassageDelete;
@@ -64,16 +64,16 @@ class PassagesListWidget extends StatefulWidget {
/// Plage de dates personnalisée pour le filtrage (utilisé si periodFilter = 'custom')
final DateTimeRange? dateRange;
/// Méthode de tri des passages ('date' par défaut, ou 'distance' pour le mode terrain)
final String? sortBy;
/// Widgets personnalisés pour les boutons de tri à afficher dans le header
final Widget? sortingButtons;
/// Si vrai, affiche un bouton pour ajouter un nouveau passage
final bool showAddButton;
/// Callback appelé lorsque le bouton d'ajout est cliqué
final VoidCallback? onAddPassage;
@@ -126,7 +126,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
_searchQuery = widget.initialSearchQuery ?? '';
_searchController.text = _searchQuery;
}
// Vérifier si l'amicale autorise la suppression des passages
bool _canDeletePassages() {
try {
@@ -135,11 +135,12 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
return amicale.chkUserDeletePass == true;
}
} catch (e) {
debugPrint('Erreur lors de la vérification des permissions de suppression: $e');
debugPrint(
'Erreur lors de la vérification des permissions de suppression: $e');
}
return false;
}
// Gestion du clic sur un passage avec flux conditionnel
void _handlePassageClick(Map<String, dynamic> passage) {
// Si un callback personnalisé est fourni, l'utiliser
@@ -151,11 +152,11 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
// Sinon, utiliser le flux conditionnel par défaut
final int passageType = passage['type'] as int? ?? 1;
final int passageId = passage['id'] as int;
// Récupérer le PassageModel depuis Hive
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
final passageModel = passagesBox.get(passageId);
if (passageModel == null) {
ApiException.showError(context, Exception('Passage introuvable'));
return;
@@ -171,14 +172,15 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
}
// Afficher le dialog de détails avec option de modification
void _showDetailsDialogWithEditOption(BuildContext context, Map<String, dynamic> passage, PassageModel passageModel) {
void _showDetailsDialogWithEditOption(BuildContext context,
Map<String, dynamic> passage, PassageModel passageModel) {
final int passageId = passage['id'] as int;
final DateTime date = passage['date'] as DateTime;
final theme = Theme.of(context);
final int passageType = passage['type'] as int? ?? 1;
final typeInfo = AppKeys.typesPassages[passageType];
final paymentInfo = AppKeys.typesReglements[passage['payment']];
showDialog(
context: context,
builder: (BuildContext dialogContext) {
@@ -191,7 +193,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: theme.dividerColor.withOpacity(0.3),
color: theme.dividerColor.withValues(alpha: 0.3),
width: 1,
),
),
@@ -202,7 +204,8 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
width: 40,
height: 40,
decoration: BoxDecoration(
color: Color(typeInfo?['couleur1'] ?? Colors.blue.value).withOpacity(0.1),
color: Color(typeInfo?['couleur1'] ?? Colors.blue.value)
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
@@ -224,16 +227,20 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
),
const SizedBox(height: 2),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Color(typeInfo?['couleur1'] ?? Colors.blue.value).withOpacity(0.1),
color:
Color(typeInfo?['couleur1'] ?? Colors.blue.value)
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
typeInfo?['titre'] ?? 'Inconnu',
style: TextStyle(
color: Color(typeInfo?['couleur1'] ?? Colors.blue.value),
fontSize: 12,
color: Color(
typeInfo?['couleur1'] ?? Colors.blue.value),
fontSize: AppTheme.r(context, 12),
fontWeight: FontWeight.w600,
),
),
@@ -257,44 +264,50 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceVariant.withOpacity(0.3),
color: theme.colorScheme.surfaceContainerHighest
.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
_buildDetailRow('Adresse', passage['address'] as String? ?? '', Icons.home),
if (passage.containsKey('name') && passage['name'] != null && (passage['name'] as String).isNotEmpty)
_buildDetailRow('Nom', passage['name'] as String, Icons.person),
_buildDetailRow('Adresse',
passage['address'] as String? ?? '', Icons.home),
if (passage.containsKey('name') &&
passage['name'] != null &&
(passage['name'] as String).isNotEmpty)
_buildDetailRow(
'Nom', passage['name'] as String, Icons.person),
],
),
),
const SizedBox(height: 20),
// Section Informations
_buildSectionHeader(Icons.info, 'Informations', theme),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceVariant.withOpacity(0.3),
color: theme.colorScheme.surfaceContainerHighest
.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
_buildDetailRow(
'Date',
'${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year} à ${date.hour}h${date.minute.toString().padLeft(2, '0')}',
Icons.calendar_today
),
'Date',
'${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year} à ${date.hour}h${date.minute.toString().padLeft(2, '0')}',
Icons.calendar_today),
_buildDetailRow(
'Montant',
'${passage['amount']?.toStringAsFixed(2) ?? '0.00'}',
Icons.euro
),
'Montant',
'${passage['amount']?.toStringAsFixed(2) ?? '0.00'}',
Icons.euro),
Row(
children: [
Icon(Icons.payment, size: 16, color: theme.colorScheme.onSurfaceVariant),
Icon(Icons.payment,
size: 16,
color: theme.colorScheme.onSurfaceVariant),
const SizedBox(width: 8),
Expanded(
child: Text(
@@ -306,16 +319,20 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Color(paymentInfo?['couleur'] ?? Colors.grey.value).withOpacity(0.1),
color: Color(paymentInfo?['couleur'] ??
Colors.grey.value)
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Text(
paymentInfo?['titre'] ?? 'Inconnu',
style: TextStyle(
color: Color(paymentInfo?['couleur'] ?? Colors.grey.value),
fontSize: 12,
color: Color(paymentInfo?['couleur'] ??
Colors.grey.value),
fontSize: AppTheme.r(context, 12),
fontWeight: FontWeight.w600,
),
),
@@ -325,9 +342,11 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
],
),
),
// Section Notes (si présentes)
if (passage.containsKey('notes') && passage['notes'] != null && (passage['notes'] as String).isNotEmpty) ...[
if (passage.containsKey('notes') &&
passage['notes'] != null &&
(passage['notes'] as String).isNotEmpty) ...[
const SizedBox(height: 20),
_buildSectionHeader(Icons.note, 'Notes', theme),
const SizedBox(height: 12),
@@ -335,24 +354,25 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.amber.withOpacity(0.05),
color: Colors.amber.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.amber.withOpacity(0.2),
color: Colors.amber.withValues(alpha: 0.2),
width: 1,
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.comment, size: 16, color: Colors.amber[700]),
Icon(Icons.comment,
size: 16, color: Colors.amber[700]),
const SizedBox(width: 8),
Expanded(
child: Text(
passage['notes'] as String,
style: TextStyle(
color: theme.colorScheme.onSurface,
fontSize: 14,
fontSize: AppTheme.r(context, 14),
),
),
),
@@ -380,7 +400,8 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
),
),
onPressed: () {
Navigator.of(dialogContext).pop(); // Fermer le dialog de détails
Navigator.of(dialogContext)
.pop(); // Fermer le dialog de détails
_showEditDialog(context, passageModel); // Ouvrir le formulaire
},
),
@@ -389,7 +410,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
},
);
}
// Helper pour construire un header de section
Widget _buildSectionHeader(IconData icon, String title, ThemeData theme) {
return Row(
@@ -406,7 +427,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
],
);
}
// Helper pour construire une ligne de détail avec icône
Widget _buildDetailRow(String label, String value, [IconData? icon]) {
final theme = Theme.of(context);
@@ -462,7 +483,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
},
);
}
// Gérer l'ajout d'un nouveau passage
void _handleAddPassage() {
// Si un callback personnalisé est fourni, l'utiliser
@@ -470,7 +491,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
widget.onAddPassage!();
return;
}
// Sinon, ouvrir directement le dialog de création
showDialog(
context: context,
@@ -494,7 +515,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
void _showDeleteConfirmationDialog(Map<String, dynamic> passage) {
final TextEditingController confirmController = TextEditingController();
String? streetNumber;
// Extraire le numéro de rue de l'adresse
try {
final address = passage['address'] as String? ?? '';
@@ -506,7 +527,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
} catch (e) {
debugPrint('Erreur extraction numéro de rue: $e');
}
showDialog(
context: context,
barrierDismissible: false,
@@ -524,12 +545,12 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
Text(
'ATTENTION : Cette action est irréversible !',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red,
fontSize: 16,
fontSize: AppTheme.r(context, 16),
),
),
const SizedBox(height: 16),
@@ -547,9 +568,9 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
),
child: Text(
passage['address'] as String? ?? 'Adresse inconnue',
style: const TextStyle(
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
fontSize: AppTheme.r(context, 14),
),
),
),
@@ -563,7 +584,9 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
controller: confirmController,
decoration: InputDecoration(
labelText: 'Numéro de rue',
hintText: streetNumber != null ? 'Ex: $streetNumber' : 'Saisir le numéro',
hintText: streetNumber != null
? 'Ex: $streetNumber'
: 'Saisir le numéro',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.home),
),
@@ -594,8 +617,9 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
);
return;
}
if (streetNumber != null && enteredNumber.toUpperCase() != streetNumber.toUpperCase()) {
if (streetNumber != null &&
enteredNumber.toUpperCase() != streetNumber.toUpperCase()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Le numéro de rue ne correspond pas'),
@@ -604,11 +628,11 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
);
return;
}
// Fermer le dialog
confirmController.dispose();
Navigator.of(dialogContext).pop();
// Effectuer la suppression
await _deletePassage(passage);
},
@@ -623,7 +647,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
},
);
}
// Supprimer un passage
Future<void> _deletePassage(Map<String, dynamic> passage) async {
try {
@@ -632,25 +656,27 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
if (passageId == null) {
throw Exception('ID du passage non trouvé');
}
// Convertir l'ID en int si nécessaire
final int id = passageId is String ? int.parse(passageId) : passageId as int;
final int id =
passageId is String ? int.parse(passageId) : passageId as int;
// Appeler le repository pour supprimer via l'API
final success = await passageRepository.deletePassageViaApi(id);
if (success && mounted) {
ApiException.showSuccess(context, 'Passage supprimé avec succès');
// Appeler le callback si défini
if (widget.onPassageDelete != null) {
widget.onPassageDelete!(passage);
}
// Forcer le rafraîchissement de la liste
setState(() {});
} else if (mounted) {
ApiException.showError(context, Exception('Erreur lors de la suppression'));
ApiException.showError(
context, Exception('Erreur lors de la suppression'));
}
} catch (e) {
debugPrint('Erreur suppression passage: $e');
@@ -689,7 +715,8 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
}
// Limiter le nombre de passages si maxPassages est défini
if (widget.maxPassages != null && filtered.length > widget.maxPassages!) {
if (widget.maxPassages != null &&
filtered.length > widget.maxPassages!) {
filtered = filtered.sublist(0, widget.maxPassages!);
}
@@ -801,7 +828,8 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
if (a.containsKey('distance') && b.containsKey('distance')) {
final double distanceA = a['distance'] as double;
final double distanceB = b['distance'] as double;
return distanceA.compareTo(distanceB); // Ordre croissant pour la distance
return distanceA
.compareTo(distanceB); // Ordre croissant pour la distance
}
return 0;
});
@@ -845,7 +873,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
if (passage.containsKey('type') && passage['type'] == 2) {
return true; // Tous les membres peuvent agir sur les passages type 2
}
// Utiliser directement le champ isOwnedByCurrentUser s'il existe
if (passage.containsKey('isOwnedByCurrentUser')) {
return passage['isOwnedByCurrentUser'] == true;
@@ -869,8 +897,6 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
final double amount =
passage.containsKey('amount') ? passage['amount'] as double : 0.0;
final bool hasValidAmount = amount > 0;
final bool isTypeEffectue = passage.containsKey('type') &&
passage['type'] == 1; // Type 1 = Effectué
final bool isOwnedByCurrentUser = _isPassageOwnedByCurrentUser(passage);
// Déterminer si nous sommes dans une page admin (pas de filterByUserId)
@@ -878,13 +904,6 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
// Dans les pages admin, tous les passages sont affichés normalement
// Dans les pages user, seuls les passages de l'utilisateur courant sont affichés normalement
final bool shouldGreyOut = !isAdminPage && !isOwnedByCurrentUser;
// Définir des styles différents en fonction du propriétaire du passage et du type de page
final TextStyle? baseTextStyle = shouldGreyOut
? theme.textTheme.bodyMedium
?.copyWith(color: theme.colorScheme.onSurface.withOpacity(0.5))
: theme.textTheme.bodyMedium;
return Row(
children: [
@@ -899,16 +918,18 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
Icon(
Icons.calendar_today,
size: 15,
color: theme.colorScheme.onSurface.withOpacity(0.7),
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
const SizedBox(width: 4),
Text(
passage.containsKey('date')
? dateFormat.format(passage['date'] as DateTime)
: 'Date non disponible',
style: theme.textTheme.bodyMedium?.copyWith( // Changé de bodySmall à bodyMedium
color: theme.colorScheme.onSurface.withOpacity(0.75),
fontSize: 14, // Taille explicite
style: theme.textTheme.bodyMedium?.copyWith(
// Changé de bodySmall à bodyMedium
color:
theme.colorScheme.onSurface.withValues(alpha: 0.75),
fontSize: AppTheme.r(context, 14), // Taille explicite
fontWeight: FontWeight.w500, // Un peu plus gras
),
),
@@ -925,15 +946,19 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
Icon(
Icons.person,
size: 16,
color: theme.colorScheme.onSurface.withOpacity(0.7),
color:
theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
const SizedBox(width: 4),
Flexible(
child: Text(
passage['name'] as String,
style: theme.textTheme.bodyMedium?.copyWith( // Changé pour être plus visible
color: theme.colorScheme.onSurface.withOpacity(0.8),
fontSize: 14, // Taille explicite
style: theme.textTheme.bodyMedium?.copyWith(
// Changé pour être plus visible
color: theme.colorScheme.onSurface
.withValues(alpha: 0.8),
fontSize:
AppTheme.r(context, 14), // Taille explicite
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
@@ -947,13 +972,15 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
Icon(
Icons.euro,
size: 16,
color: theme.colorScheme.onSurface.withOpacity(0.6),
color:
theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
const SizedBox(width: 4),
Text(
'${passage['amount']}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
color: theme.colorScheme.onSurface
.withValues(alpha: 0.6),
fontWeight: FontWeight.bold,
),
),
@@ -964,14 +991,14 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Color(typeReglement['couleur'] as int)
.withOpacity(0.1),
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
typeReglement['titre'] as String,
style: TextStyle(
color: Color(typeReglement['couleur'] as int),
fontSize: 12,
fontSize: AppTheme.r(context, 12),
fontWeight: FontWeight.w500,
),
),
@@ -994,12 +1021,11 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
}
// Construction d'une carte pour un passage (mode compact uniquement)
Widget _buildPassageCard(
Map<String, dynamic> passage, ThemeData theme) {
Widget _buildPassageCard(Map<String, dynamic> passage, ThemeData theme) {
try {
// Vérification des données et valeurs par défaut
final int type = passage.containsKey('type') ? passage['type'] as int : 1;
// S'assurer que le type existe dans la map, sinon utiliser type 1 par défaut
final Map<String, dynamic> typePassage =
AppKeys.typesPassages[type] ?? AppKeys.typesPassages[1]!;
@@ -1025,18 +1051,16 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
borderRadius: BorderRadius.circular(16),
),
// Toujours fond blanc, avec opacité réduite si grisé
color: shouldGreyOut
? Colors.white.withOpacity(0.7)
: Colors.white,
color:
shouldGreyOut ? Colors.white.withValues(alpha: 0.7) : Colors.white,
child: InkWell(
// Rendre le passage cliquable uniquement s'il appartient à l'utilisateur courant
// ou si nous sommes dans la page admin
onTap: isClickable
? () => _handlePassageClick(passage)
: null,
onTap: isClickable ? () => _handlePassageClick(passage) : null,
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0), // Réduit de 16 à 12/10
padding: const EdgeInsets.symmetric(
horizontal: 12.0, vertical: 10.0), // Réduit de 16 à 12/10
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -1049,7 +1073,7 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
height: 36,
decoration: BoxDecoration(
color: Color(typePassage['couleur1'] as int)
.withOpacity(0.1),
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Color(typePassage['couleur2'] as int),
@@ -1059,7 +1083,8 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
child: Icon(
typePassage['icon_data'] as IconData,
color: Color(typePassage['couleur1'] as int),
size: 20, // Légèrement réduit pour tenir compte de la bordure
size:
20, // Légèrement réduit pour tenir compte de la bordure
),
),
const SizedBox(width: 10),
@@ -1082,10 +1107,11 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
// Badge du type de passage
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 3), // Réduit de 8/4 à 6/3
horizontal: 6,
vertical: 3), // Réduit de 8/4 à 6/3
decoration: BoxDecoration(
color: Color(typePassage['couleur1'] as int)
.withOpacity(0.1),
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
@@ -1094,29 +1120,35 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
color:
Color(typePassage['couleur1'] as int),
fontWeight: FontWeight.bold,
fontSize: 11, // Réduit de 12 à 11
fontSize: AppTheme.r(context, 11),
),
),
),
// Boutons d'action
if (widget.showActions) ...[
// Bouton PDF pour les passages effectués
if (type == 1 && widget.onReceiptView != null && isOwnedByCurrentUser)
if (type == 1 &&
widget.onReceiptView != null &&
isOwnedByCurrentUser)
IconButton(
icon: const Icon(Icons.picture_as_pdf, size: 20),
icon: const Icon(Icons.picture_as_pdf,
size: 20),
color: Colors.green,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
onPressed: () => widget.onReceiptView!(passage),
onPressed: () =>
widget.onReceiptView!(passage),
),
// Bouton suppression si autorisé
if (_canDeletePassages() && isOwnedByCurrentUser)
if (_canDeletePassages() &&
isOwnedByCurrentUser)
IconButton(
icon: const Icon(Icons.delete, size: 20),
color: Colors.red,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(),
onPressed: () => _showDeleteConfirmationDialog(passage),
onPressed: () =>
_showDeleteConfirmationDialog(passage),
),
],
],
@@ -1134,11 +1166,12 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
),
const SizedBox(width: 4),
Text(
_formatDistance(passage['distance'] as double),
_formatDistance(
passage['distance'] as double),
style: TextStyle(
color: Colors.green[700],
fontWeight: FontWeight.w500,
fontSize: 13,
fontSize: AppTheme.r(context, 13),
),
),
],
@@ -1163,7 +1196,8 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
'Notes: ${passage['notes']}',
style: theme.textTheme.bodyMedium?.copyWith(
fontStyle: FontStyle.italic,
color: theme.colorScheme.onSurface.withOpacity(0.7),
color:
theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
),
),
@@ -1334,152 +1368,160 @@ class _PassagesListWidgetState extends State<PassagesListWidget> {
child: Container(
decoration: BoxDecoration(
// Fond transparent si c'est pour le dashboard (pas de filtres ni recherche)
color: (!widget.showFilters && !widget.showSearch)
? Colors.transparent
: Colors.transparent,
color: (!widget.showFilters && !widget.showSearch)
? Colors.transparent
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
border: (!widget.showFilters && !widget.showSearch)
? Border.all(color: Colors.transparent) // Pas de bordure pour le dashboard
: Border.all(
color: theme.colorScheme.outline.withOpacity(0.2),
width: 1,
),
boxShadow: (!widget.showFilters && !widget.showSearch)
? [] // Pas d'ombre pour le dashboard
: [
BoxShadow(
color: theme.shadowColor.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
? Border.all(
color: Colors
.transparent) // Pas de bordure pour le dashboard
: Border.all(
color: theme.colorScheme.outline.withValues(alpha: 0.2),
width: 1,
),
],
boxShadow: (!widget.showFilters && !widget.showSearch)
? [] // Pas d'ombre pour le dashboard
: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
// Header avec le nombre de passages trouvés
Container(
width: double.infinity,
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
// Header toujours avec fond coloré
color: Color.alphaBlend(
theme.colorScheme.primary.withOpacity(0.1),
theme.colorScheme.surface,
children: [
// Header avec le nombre de passages trouvés
Container(
width: double.infinity,
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
// Header toujours avec fond coloré
color: Color.alphaBlend(
theme.colorScheme.primary.withValues(alpha: 0.1),
theme.colorScheme.surface,
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.list_alt,
size: 20,
color: theme.colorScheme.primary,
),
const SizedBox(width: 8),
Text(
widget.maxPassages != null && widget.maxPassages! <= 20 && !widget.showFilters && !widget.showSearch
? '${_filteredPassages.length} dernier${_filteredPassages.length > 1 ? 's' : ''} passage${_filteredPassages.length > 1 ? 's' : ''} trouvé${_filteredPassages.length > 1 ? 's' : ''}'
: '${_filteredPassages.length} passage${_filteredPassages.length > 1 ? 's' : ''} trouvé${_filteredPassages.length > 1 ? 's' : ''}',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.list_alt,
size: 20,
color: theme.colorScheme.primary,
),
),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.sortingButtons != null) ...[
widget.sortingButtons!,
const SizedBox(width: 8),
],
if (widget.showAddButton)
Container(
height: 36,
width: 36,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: Colors.green.withOpacity(0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
Text(
widget.maxPassages != null &&
widget.maxPassages! <= 20 &&
!widget.showFilters &&
!widget.showSearch
? '${_filteredPassages.length} dernier${_filteredPassages.length > 1 ? 's' : ''} passage${_filteredPassages.length > 1 ? 's' : ''}'
: '${_filteredPassages.length} passage${_filteredPassages.length > 1 ? 's' : ''}',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
child: Material(
color: Colors.transparent,
child: InkWell(
),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.sortingButtons != null) ...[
widget.sortingButtons!,
const SizedBox(width: 8),
],
if (widget.showAddButton)
Container(
height: 36,
width: 36,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(18),
onTap: _handleAddPassage,
child: const Tooltip(
message: 'Nouveau passage',
child: Icon(
Icons.add,
color: Colors.white,
size: 24,
boxShadow: [
BoxShadow(
color: Colors.green.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(18),
onTap: _handleAddPassage,
child: const Tooltip(
message: 'Nouveau passage',
child: Icon(
Icons.add,
color: Colors.white,
size: 24,
),
),
),
),
),
),
],
),
],
),
),
// Contenu de la liste
Expanded(
child: _filteredPassages.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: theme.colorScheme.onSurface.withOpacity(0.3),
),
const SizedBox(height: 16),
Text(
'Aucun passage trouvé',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.5),
),
),
const SizedBox(height: 8),
Text(
'Essayez de modifier vos filtres de recherche',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.5),
),
),
],
),
),
)
: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: _filteredPassages.length,
itemBuilder: (context, index) {
final passage = _filteredPassages[index];
return _buildPassageCard(passage, theme);
},
],
),
),
],
),
],
),
),
// Contenu de la liste
Expanded(
child: _filteredPassages.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: theme.colorScheme.onSurface
.withValues(alpha: 0.3),
),
const SizedBox(height: 16),
Text(
'Aucun passage trouvé',
style: theme.textTheme.titleLarge?.copyWith(
color: theme.colorScheme.onSurface
.withValues(alpha: 0.5),
),
),
const SizedBox(height: 8),
Text(
'Essayez de modifier vos filtres de recherche',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface
.withValues(alpha: 0.5),
),
),
],
),
),
)
: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: _filteredPassages.length,
itemBuilder: (context, index) {
final passage = _filteredPassages[index];
return _buildPassageCard(passage, theme);
},
),
),
],
),
),
),
],

View File

@@ -164,7 +164,7 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
data: theme.copyWith(
colorScheme: theme.colorScheme.copyWith(
onSecondaryContainer: selectedColor, // Couleur de l'icône sélectionnée
secondaryContainer: selectedColor.withOpacity(0.15), // Couleur de fond de l'indicateur
secondaryContainer: selectedColor.withValues(alpha: 0.15), // Couleur de fond de l'indicateur
),
),
child: NavigationBar(
@@ -359,7 +359,7 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
// 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);
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;
@@ -401,7 +401,7 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
height: 50,
decoration: BoxDecoration(
color: isSelected
? selectedColor.withOpacity(0.1)
? selectedColor.withValues(alpha: 0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
@@ -422,7 +422,7 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
),
),
tileColor:
isSelected ? selectedColor.withOpacity(0.1) : null,
isSelected ? selectedColor.withValues(alpha: 0.1) : null,
onTap: () {
widget.onDestinationSelected(index);
},

View File

@@ -9,6 +9,7 @@ import 'package:geosector_app/core/services/current_user_service.dart';
// Enum pour les types de tri
enum SortType { name, count, progress }
enum SortOrder { none, asc, desc }
class SectorDistributionCard extends StatefulWidget {
@@ -51,16 +52,20 @@ 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 isActive =
_currentSortType == sortType && _currentSortOrder != SortOrder.none;
final isAsc =
_currentSortType == sortType && _currentSortOrder == SortOrder.asc;
return InkWell(
onTap: () => _onSortPressed(sortType),
borderRadius: BorderRadius.circular(4),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: isActive ? Colors.blue.withOpacity(0.1) : Colors.grey.withOpacity(0.1),
color: isActive
? Colors.blue.withValues(alpha: 0.1)
: Colors.grey.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: isActive ? Colors.blue : Colors.grey[400]!,
@@ -73,7 +78,7 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
Text(
label,
style: TextStyle(
fontSize: 12,
fontSize: AppTheme.r(context, 12),
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
color: isActive ? Colors.blue : Colors.grey[700],
),
@@ -111,9 +116,9 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
children: [
Text(
widget.title,
style: const TextStyle(
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
fontSize: AppTheme.r(context, 16),
),
),
// Boutons de tri groupés
@@ -208,13 +213,13 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
// Préparer les données pour l'affichage - AFFICHER TOUS LES SECTEURS
List<Map<String, dynamic>> stats = [];
for (final sector in sectors) {
// Compter les passages par type pour ce secteur
Map<int, int> passagesByType = {};
int totalCount = 0;
int passagesNotType2 = 0;
// Compter tous les passages pour ce secteur
for (final passage in passages) {
if (passage.fkSector == sector.id) {
@@ -226,12 +231,11 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
}
}
}
// Calculer le pourcentage d'avancement
final int progressPercentage = totalCount > 0
? ((passagesNotType2 / totalCount) * 100).round()
: 0;
final int progressPercentage =
totalCount > 0 ? ((passagesNotType2 / totalCount) * 100).round() : 0;
stats.add({
'id': sector.id,
'name': sector.libelle,
@@ -240,8 +244,7 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
'progressPercentage': progressPercentage,
'color': sector.color.isEmpty
? 0xFF4B77BE
: int.tryParse(sector.color.replaceAll('#', '0xFF')) ??
0xFF4B77BE,
: int.tryParse(sector.color.replaceAll('#', '0xFF')) ?? 0xFF4B77BE,
});
}
@@ -274,7 +277,8 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
break;
case SortType.progress:
stats.sort((a, b) {
final result = (a['progressPercentage'] as int).compareTo(b['progressPercentage'] as int);
final result = (a['progressPercentage'] as int)
.compareTo(b['progressPercentage'] as int);
return _currentSortOrder == SortOrder.asc ? result : -result;
});
break;
@@ -293,14 +297,14 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
final Map<int, int> passagesByType = sectorData['passagesByType'] ?? {};
final int progressPercentage = sectorData['progressPercentage'] ?? 0;
final int sectorId = sectorData['id'] ?? 0;
// Calculer le ratio par rapport au maximum (éviter division par zéro)
final double widthRatio = maxCount > 0 ? count / maxCount : 0;
// Style différent pour les secteurs sans passages
final bool hasPassages = count > 0;
final textColor = hasPassages ? Colors.black87 : Colors.grey;
// Vérifier si l'utilisateur est admin
final bool isAdmin = CurrentUserService.instance.canAccessAdmin;
@@ -320,19 +324,21 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
// Sauvegarder le secteur sélectionné et l'index de la page carte dans Hive
final settingsBox = Hive.box(AppKeys.settingsBoxName);
settingsBox.put('selectedSectorId', sectorId);
settingsBox.put('selectedPageIndex', 4); // Index de la page carte
settingsBox.put(
'selectedPageIndex', 4); // Index de la page carte
// Naviguer vers le dashboard admin qui chargera la page carte
context.go('/admin');
},
child: Text(
name,
style: TextStyle(
fontSize: 14,
fontSize: AppTheme.r(context, 14),
color: textColor,
fontWeight: hasPassages ? FontWeight.w600 : FontWeight.w300,
fontWeight:
hasPassages ? FontWeight.w600 : FontWeight.w300,
decoration: TextDecoration.underline,
decorationColor: textColor.withOpacity(0.5),
decorationColor: textColor.withValues(alpha: 0.5),
),
overflow: TextOverflow.ellipsis,
),
@@ -340,20 +346,21 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
: Text(
name,
style: TextStyle(
fontSize: 14,
fontSize: AppTheme.r(context, 14),
color: textColor,
fontWeight: hasPassages ? FontWeight.w600 : FontWeight.w300,
fontWeight:
hasPassages ? FontWeight.w600 : FontWeight.w300,
),
overflow: TextOverflow.ellipsis,
),
),
Text(
hasPassages
? '$count passages ($progressPercentage% d\'avancement)'
hasPassages
? '$count passages ($progressPercentage% d\'avancement)'
: '0 passage',
style: TextStyle(
fontWeight: hasPassages ? FontWeight.bold : FontWeight.normal,
fontSize: 13,
fontSize: AppTheme.r(context, 13),
color: textColor,
),
),
@@ -373,7 +380,8 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
);
}
Widget _buildStackedBar(Map<int, int> passagesByType, int totalCount, int sectorId, String sectorName) {
Widget _buildStackedBar(Map<int, int> passagesByType, int totalCount,
int sectorId, String sectorName) {
if (totalCount == 0) {
// Barre vide pour les secteurs sans passages
return Container(
@@ -387,7 +395,7 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
// Ordre des types : 1, 3, 4, 5, 6, 7, 8, 9, puis 2 en dernier
final typeOrder = [1, 3, 4, 5, 6, 7, 8, 9, 2];
return Container(
height: 24,
decoration: BoxDecoration(
@@ -405,40 +413,45 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
children: typeOrder.map((typeId) {
final count = passagesByType[typeId] ?? 0;
if (count == 0) return const SizedBox.shrink();
final percentage = (count / totalCount) * 100;
final typeInfo = AppKeys.typesPassages[typeId];
final color = typeInfo != null
final color = typeInfo != null
? Color(typeInfo['couleur2'] as int)
: Colors.grey;
// Vérifier si l'utilisateur est admin pour les clics
final bool isAdmin = CurrentUserService.instance.canAccessAdmin;
return Expanded(
flex: count,
child: isAdmin
? InkWell(
onTap: () {
// Sauvegarder les filtres dans Hive pour la page historique
final settingsBox = Hive.box(AppKeys.settingsBoxName);
settingsBox.put('history_selectedSectorId', sectorId);
settingsBox.put('history_selectedSectorName', sectorName);
final settingsBox =
Hive.box(AppKeys.settingsBoxName);
settingsBox.put(
'history_selectedSectorId', sectorId);
settingsBox.put(
'history_selectedSectorName', sectorName);
settingsBox.put('history_selectedTypeId', typeId);
settingsBox.put('selectedPageIndex', 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');
},
child: Container(
color: color,
child: Center(
child: percentage >= 5 // N'afficher le texte que si >= 5%
child: percentage >=
5 // N'afficher le texte que si >= 5%
? Text(
'$count (${percentage.toInt()}%)',
style: const TextStyle(
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontSize: AppTheme.r(context, 10),
fontWeight: FontWeight.bold,
shadows: [
Shadow(
@@ -456,12 +469,13 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
: Container(
color: color,
child: Center(
child: percentage >= 5 // N'afficher le texte que si >= 5%
child: percentage >=
5 // N'afficher le texte que si >= 5%
? Text(
'$count (${percentage.toInt()}%)',
style: const TextStyle(
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontSize: AppTheme.r(context, 10),
fontWeight: FontWeight.bold,
shadows: [
Shadow(
@@ -483,4 +497,4 @@ class _SectorDistributionCardState extends State<SectorDistributionCard> {
),
);
}
}
}

View File

@@ -185,10 +185,10 @@ class ThemeInfo extends StatelessWidget {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: theme.colorScheme.outline.withOpacity(0.3),
color: theme.colorScheme.outline.withValues(alpha: 0.3),
),
),
child: Row(

View File

@@ -197,22 +197,26 @@ class _UserFormState extends State<UserForm> {
}).catchError((error) {
// Gérer les erreurs spécifiques au sélecteur de date
debugPrint('Erreur lors de la sélection de la date: $error');
ScaffoldMessenger.of(context).showSnackBar(
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Erreur lors de la sélection de la date'),
backgroundColor: Colors.red,
),
);
}
});
} catch (e) {
// Gérer toutes les autres erreurs
debugPrint('Exception lors de l\'affichage du sélecteur de date: $e');
ScaffoldMessenger.of(context).showSnackBar(
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Impossible d\'afficher le sélecteur de date'),
backgroundColor: Colors.red,
),
);
}
}
}

View File

@@ -282,10 +282,10 @@ class _ValidationExampleState extends State<ValidationExample> {
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3),
color: Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.primary.withOpacity(0.3),
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3),
),
),
child: Column(