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:
@@ -99,6 +99,35 @@ class _GeosectorAppState extends State<GeosectorApp> with WidgetsBindingObserver
|
||||
themeMode: themeService.themeMode,
|
||||
routerConfig: _createRouter(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
// Builder pour appliquer le theme responsive à toute l'app
|
||||
builder: (context, child) {
|
||||
return MediaQuery(
|
||||
// Conserver les données MediaQuery existantes
|
||||
data: MediaQuery.of(context),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
// Récupérer le theme actuel (clair ou sombre)
|
||||
final brightness = Theme.of(context).brightness;
|
||||
final textColor = brightness == Brightness.light
|
||||
? AppTheme.textLightColor
|
||||
: AppTheme.textDarkColor;
|
||||
|
||||
// Débogage en mode développement
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final scaleFactor = AppTheme.getFontScaleFactor(width);
|
||||
debugPrint('📱 Largeur écran: ${width.toStringAsFixed(0)}px → Facteur: ×$scaleFactor');
|
||||
|
||||
// Appliquer le TextTheme responsive
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
textTheme: AppTheme.getResponsiveTextTheme(context, textColor),
|
||||
),
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
// Configuration des localisations pour le français
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
|
||||
@@ -45,19 +45,12 @@ class RoomsPageEmbeddedState extends State<RoomsPageEmbedded> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isWeb = kIsWeb;
|
||||
|
||||
if (_isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
// Sur le web, afficher la vue split
|
||||
if (isWeb) {
|
||||
return _buildWebSplitView(context);
|
||||
}
|
||||
|
||||
// Sur mobile, afficher la vue normale avec navigation
|
||||
return _buildMobileView(context);
|
||||
// Utiliser la vue split responsive pour toutes les plateformes
|
||||
return _buildResponsiveSplitView(context);
|
||||
}
|
||||
|
||||
Widget _buildMobileView(BuildContext context) {
|
||||
@@ -287,8 +280,8 @@ class RoomsPageEmbeddedState extends State<RoomsPageEmbedded> {
|
||||
_loadRooms();
|
||||
}
|
||||
|
||||
/// Méthode pour créer la vue split sur le web
|
||||
Widget _buildWebSplitView(BuildContext context) {
|
||||
/// Méthode pour créer la vue split responsive
|
||||
Widget _buildResponsiveSplitView(BuildContext context) {
|
||||
return ValueListenableBuilder<Box<Room>>(
|
||||
valueListenable: _service.roomsBox.listenable(),
|
||||
builder: (context, box, _) {
|
||||
@@ -315,14 +308,74 @@ class RoomsPageEmbeddedState extends State<RoomsPageEmbedded> {
|
||||
)
|
||||
: null;
|
||||
|
||||
// Déterminer si on est sur un petit écran
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final isSmallScreen = screenWidth < 900;
|
||||
|
||||
// Si petit écran ou mobile, disposition verticale
|
||||
if (isSmallScreen) {
|
||||
// Calculer la hauteur appropriée pour la liste des rooms
|
||||
// Sur mobile, utiliser 30% de la hauteur, sur web small screen 250px
|
||||
final roomsHeight = kIsWeb ? 250.0 : screenHeight * 0.3;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Liste des rooms en haut avec hauteur adaptative
|
||||
Container(
|
||||
height: roomsHeight.clamp(200.0, 350.0), // Entre 200 et 350 pixels
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: _buildRoomsList(context),
|
||||
),
|
||||
// Conversation sélectionnée en dessous (reste de l'espace)
|
||||
Expanded(
|
||||
child: selectedRoom != null && selectedRoom.id.isNotEmpty
|
||||
? ChatPage(
|
||||
key: ValueKey(selectedRoom.id), // Clé unique par room
|
||||
roomId: selectedRoom.id,
|
||||
roomTitle: selectedRoom.title,
|
||||
roomType: selectedRoom.type,
|
||||
roomCreatorId: selectedRoom.createdBy,
|
||||
isEmbedded: true, // Pour indiquer qu'on est en mode embedded
|
||||
)
|
||||
: _buildEmptyConversation(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Si grand écran, disposition horizontale (comme avant)
|
||||
return Row(
|
||||
children: [
|
||||
// Colonne de gauche : Liste des rooms (30%)
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width * 0.3,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 280,
|
||||
maxWidth: 400,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border(
|
||||
right: BorderSide(color: Colors.grey[300]!),
|
||||
right: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: _buildRoomsList(context),
|
||||
@@ -1189,7 +1242,7 @@ class _QuickBroadcastDialogState extends State<_QuickBroadcastDialog> {
|
||||
_isBroadcast = value;
|
||||
});
|
||||
},
|
||||
activeColor: Colors.amber.shade600,
|
||||
activeThumbColor: Colors.amber.shade600,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -111,9 +111,9 @@ class _RecipientSelectorState extends State<RecipientSelector> {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: _hexToColor(color).withOpacity(0.1),
|
||||
color: _hexToColor(color).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: _hexToColor(color).withOpacity(0.3)),
|
||||
border: Border.all(color: _hexToColor(color).withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Text(
|
||||
name,
|
||||
@@ -602,7 +602,7 @@ class _RecipientSelectorWithMessageState extends State<_RecipientSelectorWithMes
|
||||
_isBroadcast = value;
|
||||
});
|
||||
},
|
||||
activeColor: Colors.amber.shade600,
|
||||
activeThumbColor: Colors.amber.shade600,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:geosector_app/core/data/models/sector_model.dart';
|
||||
import 'package:geosector_app/core/data/models/passage_model.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart';
|
||||
|
||||
@@ -25,13 +25,6 @@ class DataLoadingService extends ChangeNotifier {
|
||||
LoadingState _loadingState = LoadingState.initial;
|
||||
LoadingState get loadingState => _loadingState;
|
||||
|
||||
// Callback pour les mises à jour de progression
|
||||
Function(LoadingState)? _progressCallback;
|
||||
|
||||
// Méthode pour définir un callback de progression
|
||||
void setProgressCallback(Function(LoadingState)? callback) {
|
||||
_progressCallback = callback;
|
||||
}
|
||||
|
||||
// === GETTERS POUR LES BOXES ===
|
||||
Box<OperationModel> get _operationBox =>
|
||||
|
||||
@@ -67,7 +67,9 @@ class LocationService {
|
||||
if (kIsWeb) {
|
||||
try {
|
||||
Position position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
),
|
||||
);
|
||||
return LatLng(position.latitude, position.longitude);
|
||||
} catch (e) {
|
||||
@@ -87,7 +89,9 @@ class LocationService {
|
||||
|
||||
// Obtenir la position actuelle
|
||||
Position position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
),
|
||||
);
|
||||
|
||||
return LatLng(position.latitude, position.longitude);
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
// Breakpoints pour le responsive design
|
||||
static const double breakpointMobileSmall = 360; // iPhone SE et petits Android
|
||||
static const double breakpointMobile = 600; // Smartphones standards
|
||||
static const double breakpointTablet = 900; // Tablettes
|
||||
|
||||
// Multiplicateurs de taille selon l'écran
|
||||
static const double fontScaleExtraSmall = 0.85; // < 360px
|
||||
static const double fontScaleSmall = 0.9; // 360-600px
|
||||
static const double fontScaleMedium = 0.95; // 600-900px
|
||||
static const double fontScaleLarge = 1.0; // > 900px
|
||||
|
||||
/// Calcule le multiplicateur de police selon la largeur d'écran
|
||||
static double getFontScaleFactor(double screenWidth) {
|
||||
if (screenWidth < breakpointMobileSmall) return fontScaleExtraSmall;
|
||||
if (screenWidth < breakpointMobile) return fontScaleSmall;
|
||||
if (screenWidth < breakpointTablet) return fontScaleMedium;
|
||||
return fontScaleLarge;
|
||||
}
|
||||
|
||||
/// Retourne une taille de police responsive
|
||||
static double responsive(BuildContext context, double baseSize) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
return baseSize * getFontScaleFactor(width);
|
||||
}
|
||||
|
||||
/// Retourne une taille de police responsive (version courte)
|
||||
static double r(BuildContext context, double baseSize) => responsive(context, baseSize);
|
||||
|
||||
// Couleurs du thème basées sur la maquette Figma
|
||||
static const Color primaryColor = Color(0xFF20335E); // Bleu foncé
|
||||
static const Color secondaryColor = Color(0xFF9DC7C8); // Bleu clair
|
||||
@@ -35,7 +63,7 @@ class AppTheme {
|
||||
// Ombres
|
||||
static List<BoxShadow> cardShadow = [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
@@ -44,7 +72,7 @@ class AppTheme {
|
||||
|
||||
static List<BoxShadow> buttonShadow = [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
@@ -130,14 +158,14 @@ class AppTheme {
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
||||
borderSide: BorderSide(
|
||||
color: textLightColor.withOpacity(0.1),
|
||||
color: textLightColor.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
||||
borderSide: BorderSide(
|
||||
color: textLightColor.withOpacity(0.1),
|
||||
color: textLightColor.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -227,14 +255,14 @@ class AppTheme {
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
||||
borderSide: BorderSide(
|
||||
color: textDarkColor.withOpacity(0.1),
|
||||
color: textDarkColor.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(borderRadiusMedium),
|
||||
borderSide: BorderSide(
|
||||
color: textDarkColor.withOpacity(0.1),
|
||||
color: textDarkColor.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -253,34 +281,126 @@ class AppTheme {
|
||||
color: const Color(0xFF1F2937),
|
||||
),
|
||||
dividerTheme: DividerThemeData(
|
||||
color: textDarkColor.withOpacity(0.1),
|
||||
color: textDarkColor.withValues(alpha: 0.1),
|
||||
thickness: 1,
|
||||
space: spacingM,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Méthode helper pour générer le TextTheme
|
||||
// Méthode helper pour générer le TextTheme responsive
|
||||
static TextTheme getResponsiveTextTheme(BuildContext context, Color textColor) {
|
||||
final scaleFactor = getFontScaleFactor(MediaQuery.of(context).size.width);
|
||||
|
||||
return TextTheme(
|
||||
// Display styles (très grandes tailles)
|
||||
displayLarge: TextStyle(
|
||||
fontFamily: 'Figtree',
|
||||
color: textColor,
|
||||
fontSize: 57 * scaleFactor, // Material 3 default
|
||||
),
|
||||
displayMedium: TextStyle(
|
||||
fontFamily: 'Figtree',
|
||||
color: textColor,
|
||||
fontSize: 45 * scaleFactor,
|
||||
),
|
||||
displaySmall: TextStyle(
|
||||
fontFamily: 'Figtree',
|
||||
color: textColor,
|
||||
fontSize: 36 * scaleFactor,
|
||||
),
|
||||
|
||||
// Headline styles (titres principaux)
|
||||
headlineLarge: TextStyle(
|
||||
fontFamily: 'Figtree',
|
||||
color: textColor,
|
||||
fontSize: 32 * scaleFactor,
|
||||
),
|
||||
headlineMedium: TextStyle(
|
||||
fontFamily: 'Figtree',
|
||||
color: textColor,
|
||||
fontSize: 28 * scaleFactor,
|
||||
),
|
||||
headlineSmall: TextStyle(
|
||||
fontFamily: 'Figtree',
|
||||
color: textColor,
|
||||
fontSize: 24 * scaleFactor,
|
||||
),
|
||||
|
||||
// Title styles (sous-titres)
|
||||
titleLarge: TextStyle(
|
||||
fontFamily: 'Figtree',
|
||||
color: textColor,
|
||||
fontSize: 22 * scaleFactor,
|
||||
),
|
||||
titleMedium: TextStyle(
|
||||
fontFamily: 'Figtree',
|
||||
color: textColor,
|
||||
fontSize: 16 * scaleFactor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
titleSmall: TextStyle(
|
||||
fontFamily: 'Figtree',
|
||||
color: textColor,
|
||||
fontSize: 14 * scaleFactor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
|
||||
// Body styles (texte principal)
|
||||
bodyLarge: TextStyle(
|
||||
fontFamily: 'Figtree',
|
||||
color: textColor,
|
||||
fontSize: 16 * scaleFactor,
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
fontFamily: 'Figtree',
|
||||
color: textColor,
|
||||
fontSize: 14 * scaleFactor,
|
||||
),
|
||||
bodySmall: TextStyle(
|
||||
fontFamily: 'Figtree',
|
||||
color: textColor.withValues(alpha: 0.7),
|
||||
fontSize: 12 * scaleFactor,
|
||||
),
|
||||
|
||||
// Label styles (petits textes, boutons)
|
||||
labelLarge: TextStyle(
|
||||
fontFamily: 'Figtree',
|
||||
color: textColor,
|
||||
fontSize: 14 * scaleFactor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
labelMedium: TextStyle(
|
||||
fontFamily: 'Figtree',
|
||||
color: textColor.withValues(alpha: 0.7),
|
||||
fontSize: 12 * scaleFactor,
|
||||
),
|
||||
labelSmall: TextStyle(
|
||||
fontFamily: 'Figtree',
|
||||
color: textColor.withValues(alpha: 0.7),
|
||||
fontSize: 11 * scaleFactor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Version statique pour compatibilité (utilise les tailles par défaut)
|
||||
static TextTheme _getTextTheme(Color textColor) {
|
||||
return TextTheme(
|
||||
displayLarge: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
displayMedium: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
displaySmall: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
headlineLarge: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
headlineMedium: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
headlineSmall: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
titleLarge: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
titleMedium: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
titleSmall: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
bodyLarge: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
bodyMedium: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
bodySmall:
|
||||
TextStyle(fontFamily: 'Figtree', color: textColor.withOpacity(0.7)),
|
||||
labelLarge: TextStyle(fontFamily: 'Figtree', color: textColor),
|
||||
labelMedium:
|
||||
TextStyle(fontFamily: 'Figtree', color: textColor.withOpacity(0.7)),
|
||||
labelSmall:
|
||||
TextStyle(fontFamily: 'Figtree', color: textColor.withOpacity(0.7)),
|
||||
displayLarge: TextStyle(fontFamily: 'Figtree', color: textColor, fontSize: 57),
|
||||
displayMedium: TextStyle(fontFamily: 'Figtree', color: textColor, fontSize: 45),
|
||||
displaySmall: TextStyle(fontFamily: 'Figtree', color: textColor, fontSize: 36),
|
||||
headlineLarge: TextStyle(fontFamily: 'Figtree', color: textColor, fontSize: 32),
|
||||
headlineMedium: TextStyle(fontFamily: 'Figtree', color: textColor, fontSize: 28),
|
||||
headlineSmall: TextStyle(fontFamily: 'Figtree', color: textColor, fontSize: 24),
|
||||
titleLarge: TextStyle(fontFamily: 'Figtree', color: textColor, fontSize: 22),
|
||||
titleMedium: TextStyle(fontFamily: 'Figtree', color: textColor, fontSize: 16, fontWeight: FontWeight.w500),
|
||||
titleSmall: TextStyle(fontFamily: 'Figtree', color: textColor, fontSize: 14, fontWeight: FontWeight.w500),
|
||||
bodyLarge: TextStyle(fontFamily: 'Figtree', color: textColor, fontSize: 16),
|
||||
bodyMedium: TextStyle(fontFamily: 'Figtree', color: textColor, fontSize: 14),
|
||||
bodySmall: TextStyle(fontFamily: 'Figtree', color: textColor.withValues(alpha: 0.7), fontSize: 12),
|
||||
labelLarge: TextStyle(fontFamily: 'Figtree', color: textColor, fontSize: 14, fontWeight: FontWeight.w500),
|
||||
labelMedium: TextStyle(fontFamily: 'Figtree', color: textColor.withValues(alpha: 0.7), fontSize: 12),
|
||||
labelSmall: TextStyle(fontFamily: 'Figtree', color: textColor.withValues(alpha: 0.7), fontSize: 11),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)'
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -42,7 +42,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
|
||||
|
||||
Reference in New Issue
Block a user