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 4153f73ace
commit f7baa7492c
106 changed files with 88501 additions and 88280 deletions

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);
},
),
),
],
),
),
),
],