feat: Mise à jour des interfaces mobiles v3.2.3

- Amélioration des interfaces utilisateur sur mobile
- Optimisation de la responsivité des composants Flutter
- Mise à jour des widgets de chat et communication
- Amélioration des formulaires et tableaux
- Ajout de nouveaux composants pour l'administration
- Optimisation des thèmes et styles visuels

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-02 20:35:40 +02:00
parent 08f4bff358
commit 43d4cd66e1
2133 changed files with 237004 additions and 173303 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/services/connectivity_service.dart';
import 'package:geosector_app/core/utils/api_exception.dart';
import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:uuid/uuid.dart';
/// Widget de test pour vérifier le fonctionnement de la file d'attente offline

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -164,7 +164,7 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
data: theme.copyWith(
colorScheme: theme.colorScheme.copyWith(
onSecondaryContainer: selectedColor, // Couleur de l'icône sélectionnée
secondaryContainer: selectedColor.withOpacity(0.15), // Couleur de fond de l'indicateur
secondaryContainer: selectedColor.withValues(alpha: 0.15), // Couleur de fond de l'indicateur
),
),
child: NavigationBar(
@@ -359,7 +359,7 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
// Définir les couleurs selon le rôle (admin = rouge, user = vert)
final Color selectedColor = widget.isAdmin ? Colors.red : Colors.green;
final Color unselectedColor = theme.colorScheme.onSurface.withOpacity(0.6);
final Color unselectedColor = theme.colorScheme.onSurface.withValues(alpha: 0.6);
// Gérer le cas où l'icône est un BadgedIcon ou autre widget composite
Widget iconWidget;
@@ -401,7 +401,7 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
height: 50,
decoration: BoxDecoration(
color: isSelected
? selectedColor.withOpacity(0.1)
? selectedColor.withValues(alpha: 0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
@@ -422,7 +422,7 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
),
),
tileColor:
isSelected ? selectedColor.withOpacity(0.1) : null,
isSelected ? selectedColor.withValues(alpha: 0.1) : null,
onTap: () {
widget.onDestinationSelected(index);
},

View File

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

View File

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

View File

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

View File

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