On release/v3.1.4: Sauvegarde temporaire pour changement de branche

This commit is contained in:
2025-08-21 17:51:22 +02:00
parent 6c8853e553
commit 41a4505b4b
1697 changed files with 167987 additions and 231472 deletions

View File

@@ -79,6 +79,9 @@ class AmicaleModel extends HiveObject {
@HiveField(24)
final String? logoBase64; // Logo en base64 (data:image/png;base64,...)
@HiveField(25)
final bool chkUserDeletePass;
AmicaleModel({
required this.id,
required this.name,
@@ -105,6 +108,7 @@ class AmicaleModel extends HiveObject {
this.chkMdpManuel = false,
this.chkUsernameManuel = false,
this.logoBase64,
this.chkUserDeletePass = false,
});
// Factory pour convertir depuis JSON (API)
@@ -139,6 +143,8 @@ class AmicaleModel extends HiveObject {
json['chk_mdp_manuel'] == 1 || json['chk_mdp_manuel'] == true;
final bool chkUsernameManuel =
json['chk_username_manuel'] == 1 || json['chk_username_manuel'] == true;
final bool chkUserDeletePass =
json['chk_user_delete_pass'] == 1 || json['chk_user_delete_pass'] == true;
// Traiter le logo si présent
String? logoBase64;
@@ -192,6 +198,7 @@ class AmicaleModel extends HiveObject {
chkMdpManuel: chkMdpManuel,
chkUsernameManuel: chkUsernameManuel,
logoBase64: logoBase64,
chkUserDeletePass: chkUserDeletePass,
);
}
@@ -222,6 +229,7 @@ class AmicaleModel extends HiveObject {
'updated_at': updatedAt?.toIso8601String(),
'chk_mdp_manuel': chkMdpManuel ? 1 : 0,
'chk_username_manuel': chkUsernameManuel ? 1 : 0,
'chk_user_delete_pass': chkUserDeletePass ? 1 : 0,
// Note: logoBase64 n'est pas envoyé via toJson (lecture seule depuis l'API)
};
}
@@ -252,6 +260,7 @@ class AmicaleModel extends HiveObject {
bool? chkMdpManuel,
bool? chkUsernameManuel,
String? logoBase64,
bool? chkUserDeletePass,
}) {
return AmicaleModel(
id: id,
@@ -279,6 +288,7 @@ class AmicaleModel extends HiveObject {
chkMdpManuel: chkMdpManuel ?? this.chkMdpManuel,
chkUsernameManuel: chkUsernameManuel ?? this.chkUsernameManuel,
logoBase64: logoBase64 ?? this.logoBase64,
chkUserDeletePass: chkUserDeletePass ?? this.chkUserDeletePass,
);
}
}

View File

@@ -42,13 +42,14 @@ class AmicaleModelAdapter extends TypeAdapter<AmicaleModel> {
chkMdpManuel: fields[22] as bool,
chkUsernameManuel: fields[23] as bool,
logoBase64: fields[24] as String?,
chkUserDeletePass: fields[25] as bool,
);
}
@override
void write(BinaryWriter writer, AmicaleModel obj) {
writer
..writeByte(25)
..writeByte(26)
..writeByte(0)
..write(obj.id)
..writeByte(1)
@@ -98,7 +99,9 @@ class AmicaleModelAdapter extends TypeAdapter<AmicaleModel> {
..writeByte(23)
..write(obj.chkUsernameManuel)
..writeByte(24)
..write(obj.logoBase64);
..write(obj.logoBase64)
..writeByte(25)
..write(obj.chkUserDeletePass);
}
@override

View File

@@ -76,6 +76,9 @@ class ClientModel extends HiveObject {
@HiveField(23)
final bool? chkUsernameManuel;
@HiveField(24)
final bool? chkUserDeletePass;
ClientModel({
required this.id,
required this.name,
@@ -101,6 +104,7 @@ class ClientModel extends HiveObject {
this.updatedAt,
this.chkMdpManuel,
this.chkUsernameManuel,
this.chkUserDeletePass,
});
// Factory pour convertir depuis JSON (API)
@@ -148,6 +152,7 @@ class ClientModel extends HiveObject {
updatedAt: json['updated_at'] != null ? DateTime.parse(json['updated_at']) : null,
chkMdpManuel: json['chk_mdp_manuel'] == 1 || json['chk_mdp_manuel'] == true,
chkUsernameManuel: json['chk_username_manuel'] == 1 || json['chk_username_manuel'] == true,
chkUserDeletePass: json['chk_user_delete_pass'] == 1 || json['chk_user_delete_pass'] == true,
);
}
@@ -178,6 +183,7 @@ class ClientModel extends HiveObject {
'updated_at': updatedAt?.toIso8601String(),
'chk_mdp_manuel': chkMdpManuel,
'chk_username_manuel': chkUsernameManuel,
'chk_user_delete_pass': chkUserDeletePass,
};
}
@@ -206,6 +212,7 @@ class ClientModel extends HiveObject {
DateTime? updatedAt,
bool? chkMdpManuel,
bool? chkUsernameManuel,
bool? chkUserDeletePass,
}) {
return ClientModel(
id: id,
@@ -232,6 +239,7 @@ class ClientModel extends HiveObject {
updatedAt: updatedAt ?? this.updatedAt,
chkMdpManuel: chkMdpManuel ?? this.chkMdpManuel,
chkUsernameManuel: chkUsernameManuel ?? this.chkUsernameManuel,
chkUserDeletePass: chkUserDeletePass ?? this.chkUserDeletePass,
);
}
}

View File

@@ -41,13 +41,14 @@ class ClientModelAdapter extends TypeAdapter<ClientModel> {
updatedAt: fields[21] as DateTime?,
chkMdpManuel: fields[22] as bool?,
chkUsernameManuel: fields[23] as bool?,
chkUserDeletePass: fields[24] as bool?,
);
}
@override
void write(BinaryWriter writer, ClientModel obj) {
writer
..writeByte(24)
..writeByte(25)
..writeByte(0)
..write(obj.id)
..writeByte(1)
@@ -95,7 +96,9 @@ class ClientModelAdapter extends TypeAdapter<ClientModel> {
..writeByte(22)
..write(obj.chkMdpManuel)
..writeByte(23)
..write(obj.chkUsernameManuel);
..write(obj.chkUsernameManuel)
..writeByte(24)
..write(obj.chkUserDeletePass);
}
@override

View File

@@ -130,6 +130,9 @@ class AmicaleRepository extends ChangeNotifier {
chkAcceptSms: amicale.chkAcceptSms,
chkActive: amicale.chkActive,
chkStripe: amicale.chkStripe,
chkMdpManuel: amicale.chkMdpManuel,
chkUsernameManuel: amicale.chkUsernameManuel,
chkUserDeletePass: amicale.chkUserDeletePass,
createdAt: amicale.createdAt ?? DateTime.now(),
updatedAt: DateTime.now(),
);

View File

@@ -382,7 +382,6 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
});
},
destinations: destinations,
showNewPassageButton: false,
isAdmin: true,
body: pages[_selectedIndex],
),

View File

@@ -1,6 +1,7 @@
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/data/models/sector_model.dart';
@@ -36,6 +37,14 @@ class DotsPainter extends CustomPainter {
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
// Enum pour gérer les types de tri
enum PassageSortType {
dateDesc, // Plus récent en premier (défaut)
dateAsc, // Plus ancien en premier
addressAsc, // Adresse A-Z
addressDesc, // Adresse Z-A
}
class AdminHistoryPage extends StatefulWidget {
const AdminHistoryPage({super.key});
@@ -52,6 +61,9 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
String selectedPaymentMethod = 'Tous';
String selectedPeriod = 'Tous'; // Période par défaut
DateTimeRange? selectedDateRange;
// État du tri actuel
PassageSortType _currentSort = PassageSortType.dateDesc;
// Contrôleur pour la recherche
final TextEditingController _searchController = TextEditingController();
@@ -215,10 +227,10 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
super.dispose();
}
// Méthode pour appliquer tous les filtres
List<Map<String, dynamic>> _getFilteredPassages() {
// Nouvelle méthode pour filtrer une liste de passages déjà formatés
List<Map<String, dynamic>> _getFilteredPassagesFromList(List<Map<String, dynamic>> passages) {
try {
var filtered = _formattedPassages.where((passage) {
var filtered = passages.where((passage) {
try {
// Ne plus exclure automatiquement les passages de type 2
// car on propose maintenant un filtre par type dans les "Filtres avancés"
@@ -315,26 +327,107 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
}
}).toList();
// Trier par date décroissante (plus récent en premier)
filtered.sort((a, b) {
try {
final DateTime dateA = a['date'] as DateTime;
final DateTime dateB = b['date'] as DateTime;
return dateB.compareTo(dateA);
} catch (e) {
return 0;
}
});
// Appliquer le tri sélectionné
filtered = _sortPassages(filtered);
debugPrint(
'Passages filtrés: ${filtered.length}/${_formattedPassages.length}');
'Passages filtrés: ${filtered.length}/${passages.length}');
return filtered;
} catch (e) {
debugPrint('Erreur globale lors du filtrage: $e');
return _formattedPassages;
return passages;
}
}
// Méthode pour trier les passages selon le type de tri sélectionné
List<Map<String, dynamic>> _sortPassages(List<Map<String, dynamic>> passages) {
final sortedPassages = List<Map<String, dynamic>>.from(passages);
switch (_currentSort) {
case PassageSortType.dateDesc:
sortedPassages.sort((a, b) {
try {
return (b['date'] as DateTime).compareTo(a['date'] as DateTime);
} catch (e) {
return 0;
}
});
break;
case PassageSortType.dateAsc:
sortedPassages.sort((a, b) {
try {
return (a['date'] as DateTime).compareTo(b['date'] as DateTime);
} catch (e) {
return 0;
}
});
break;
case PassageSortType.addressAsc:
sortedPassages.sort((a, b) {
try {
// Tri intelligent par rue, numéro (numérique), rueBis
final String rueA = a['rue'] ?? '';
final String rueB = b['rue'] ?? '';
final String numeroA = a['numero'] ?? '';
final String numeroB = b['numero'] ?? '';
final String rueBisA = a['rueBis'] ?? '';
final String rueBisB = b['rueBis'] ?? '';
// D'abord comparer les rues
int rueCompare = rueA.toLowerCase().compareTo(rueB.toLowerCase());
if (rueCompare != 0) return rueCompare;
// Si les rues sont identiques, comparer les numéros (numériquement)
int numA = int.tryParse(numeroA) ?? 0;
int numB = int.tryParse(numeroB) ?? 0;
int numCompare = numA.compareTo(numB);
if (numCompare != 0) return numCompare;
// Si les numéros sont identiques, comparer les rueBis
return rueBisA.toLowerCase().compareTo(rueBisB.toLowerCase());
} catch (e) {
return 0;
}
});
break;
case PassageSortType.addressDesc:
sortedPassages.sort((a, b) {
try {
// Tri intelligent inversé par rue, numéro (numérique), rueBis
final String rueA = a['rue'] ?? '';
final String rueB = b['rue'] ?? '';
final String numeroA = a['numero'] ?? '';
final String numeroB = b['numero'] ?? '';
final String rueBisA = a['rueBis'] ?? '';
final String rueBisB = b['rueBis'] ?? '';
// D'abord comparer les rues (inversé)
int rueCompare = rueB.toLowerCase().compareTo(rueA.toLowerCase());
if (rueCompare != 0) return rueCompare;
// Si les rues sont identiques, comparer les numéros (inversé numériquement)
int numA = int.tryParse(numeroA) ?? 0;
int numB = int.tryParse(numeroB) ?? 0;
int numCompare = numB.compareTo(numA);
if (numCompare != 0) return numCompare;
// Si les numéros sont identiques, comparer les rueBis (inversé)
return rueBisB.toLowerCase().compareTo(rueBisA.toLowerCase());
} catch (e) {
return 0;
}
});
break;
}
return sortedPassages;
}
// Méthode pour appliquer tous les filtres (utilisée dans le build)
List<Map<String, dynamic>> _getFilteredPassages() {
return _getFilteredPassagesFromList(_formattedPassages);
}
// Mettre à jour le filtre par secteur
void _updateSectorFilter(String sectorName, int? sectorId) {
setState(() {
@@ -465,29 +558,135 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
const SizedBox(height: 16),
// Widget de liste des passages avec hauteur fixe
// Widget de liste des passages avec hauteur fixe et ValueListenableBuilder
SizedBox(
height: constraints.maxHeight * 0.7, // 70% de la hauteur disponible
child: PassagesListWidget(
passages: passages,
showFilters:
false, // Désactivé car les filtres sont maintenant dans la card "Filtres avancés"
showSearch:
false, // Désactivé car la recherche est maintenant dans la card "Filtres avancés"
showActions: true,
// Ne plus passer les filtres individuels car ils sont maintenant appliqués dans _getFilteredPassages()
onPassageSelected: (passage) {
_openPassageEditDialog(context, passage);
},
onReceiptView: (passage) {
_showReceiptDialog(context, passage);
},
onDetailsView: (passage) {
_showDetailsDialog(context, passage);
},
onPassageEdit: (passage) {
// Action pour modifier le passage
// Cette fonctionnalité pourrait être implémentée ultérieurement
child: ValueListenableBuilder(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
// Reconvertir les passages à chaque changement
final List<PassageModel> allPassages = passagesBox.values.toList();
// Convertir et formater les passages
final formattedPassages = _formatPassagesForWidget(
allPassages,
_sectorRepository,
_membreRepository
);
// Appliquer les filtres
final filteredPassages = _getFilteredPassagesFromList(formattedPassages);
return PassagesListWidget(
showAddButton: true, // Activer le bouton de création
onAddPassage: () async {
// Ouvrir le dialogue de création de passage
await showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return PassageFormDialog(
title: 'Nouveau passage',
passageRepository: _passageRepository,
userRepository: _userRepository,
operationRepository: operationRepository,
onSuccess: () {
// Le widget se rafraîchira automatiquement via ValueListenableBuilder
},
);
},
);
},
sortingButtons: Row(
children: [
// Bouton tri par date avec icône calendrier
IconButton(
icon: Icon(
Icons.calendar_today,
size: 20,
color: _currentSort == PassageSortType.dateDesc ||
_currentSort == PassageSortType.dateAsc
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
tooltip: _currentSort == PassageSortType.dateAsc
? 'Tri par date (ancien en premier)'
: 'Tri par date (récent en premier)',
onPressed: () {
setState(() {
if (_currentSort == PassageSortType.dateDesc) {
_currentSort = PassageSortType.dateAsc;
} else {
_currentSort = PassageSortType.dateDesc;
}
});
},
),
// Indicateur de direction pour la date
if (_currentSort == PassageSortType.dateDesc ||
_currentSort == PassageSortType.dateAsc)
Icon(
_currentSort == PassageSortType.dateAsc
? Icons.arrow_upward
: Icons.arrow_downward,
size: 14,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 4),
// Bouton tri par adresse avec icône maison
IconButton(
icon: Icon(
Icons.home,
size: 20,
color: _currentSort == PassageSortType.addressDesc ||
_currentSort == PassageSortType.addressAsc
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
tooltip: _currentSort == PassageSortType.addressAsc
? 'Tri par adresse (A-Z)'
: 'Tri par adresse (Z-A)',
onPressed: () {
setState(() {
if (_currentSort == PassageSortType.addressAsc) {
_currentSort = PassageSortType.addressDesc;
} else {
_currentSort = PassageSortType.addressAsc;
}
});
},
),
// Indicateur de direction pour l'adresse
if (_currentSort == PassageSortType.addressDesc ||
_currentSort == PassageSortType.addressAsc)
Icon(
_currentSort == PassageSortType.addressAsc
? Icons.arrow_upward
: Icons.arrow_downward,
size: 14,
color: Theme.of(context).colorScheme.primary,
),
],
),
passages: filteredPassages,
showFilters: false,
showSearch: false,
showActions: true,
// Le widget gère maintenant le flux conditionnel par défaut
onPassageSelected: null,
onReceiptView: (passage) {
_showReceiptDialog(context, passage);
},
onDetailsView: (passage) {
_showDetailsDialog(context, passage);
},
onPassageEdit: (passage) {
// Action pour modifier le passage
},
onPassageDelete: (passage) {
_showDeleteConfirmationDialog(passage);
},
);
},
),
),
@@ -584,6 +783,9 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
// Déterminer si le passage a une erreur d'envoi de reçu
final bool hasError = passage.emailErreur.isNotEmpty;
// Récupérer l'ID de l'utilisateur courant pour déterminer la propriété
final currentUserId = _userRepository.getCurrentUser()?.id;
return {
'id': passage.id,
if (passage.passedAt != null) 'date': passage.passedAt!,
@@ -618,6 +820,7 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
'lastSyncedAt': passage.lastSyncedAt,
'isActive': passage.isActive,
'isSynced': passage.isSynced,
'isOwnedByCurrentUser': passage.fkUser == currentUserId, // Ajout du champ pour le widget
};
}).toList();
}
@@ -653,6 +856,116 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
);
}
// Afficher les détails avec option de modification
void _showDetailsDialogWithEditOption(BuildContext context, Map<String, dynamic> passage, PassageModel passageModel) {
final int passageId = passage['id'] as int;
final DateTime date = passage['date'] as DateTime;
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Row(
children: [
const Icon(Icons.info_outline, color: Colors.blue),
const SizedBox(width: 8),
Text('Détails du passage #$passageId'),
],
),
content: SizedBox(
width: 500,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_buildDetailRow('Date',
'${date.day}/${date.month}/${date.year} à ${date.hour}h${date.minute.toString().padLeft(2, '0')}'),
_buildDetailRow('Adresse', passage['address'] as String),
_buildDetailRow('Secteur', passage['sector'] as String),
_buildDetailRow('Collecteur', passage['user'] as String),
_buildDetailRow(
'Type',
AppKeys.typesPassages[passage['type']]?['titre'] ??
'Inconnu'),
_buildDetailRow('Montant', '${passage['amount']}'),
_buildDetailRow(
'Mode de paiement',
AppKeys.typesReglements[passage['payment']]?['titre'] ??
'Inconnu'),
_buildDetailRow('Email', passage['email'] as String),
_buildDetailRow(
'Reçu envoyé', passage['hasReceipt'] ? 'Oui' : 'Non'),
_buildDetailRow(
'Erreur d\'envoi', passage['hasError'] ? 'Oui' : 'Non'),
_buildDetailRow(
'Notes',
(passage['notes'] as String).isEmpty
? '-'
: passage['notes'] as String),
const SizedBox(height: 16),
const Text(
'Historique des actions',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHistoryItem(
date,
passage['user'] as String,
'Création du passage',
),
if (passage['hasReceipt'])
_buildHistoryItem(
date.add(const Duration(minutes: 5)),
'Système',
'Envoi du reçu par email',
),
if (passage['hasError'])
_buildHistoryItem(
date.add(const Duration(minutes: 6)),
'Système',
'Erreur lors de l\'envoi du reçu',
),
],
),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('Fermer'),
),
ElevatedButton.icon(
icon: const Icon(Icons.edit),
onPressed: () {
// Fermer le dialog de détails
Navigator.pop(dialogContext);
// Ouvrir le formulaire de modification
_showEditDialog(context, passageModel);
},
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
),
label: const Text('Modifier'),
),
],
),
);
}
// Méthode pour conserver l'ancienne _showDetailsDialog pour les autres usages
void _showDetailsDialog(BuildContext context, Map<String, dynamic> passage) {
final int passageId = passage['id'] as int;
final DateTime date = passage['date'] as DateTime;
@@ -736,13 +1049,6 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
onPressed: () => Navigator.pop(context),
child: const Text('Fermer'),
),
ElevatedButton(
onPressed: () {
// Action pour modifier le passage
Navigator.pop(context);
},
child: const Text('Modifier'),
),
],
),
);
@@ -753,9 +1059,10 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
try {
debugPrint('=== DEBUT _openPassageEditDialog ===');
// Récupérer l'ID du passage
// Récupérer l'ID et le type du passage
final int passageId = passage['id'] as int;
debugPrint('Recherche du passage ID: $passageId');
final int passageType = passage['type'] as int? ?? 1;
debugPrint('Passage ID: $passageId, Type: $passageType');
// Trouver le PassageModel original dans la liste
final PassageModel? passageModel =
@@ -771,23 +1078,17 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
return;
}
debugPrint('Ouverture du dialog...');
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => PassageFormDialog(
passage: passageModel,
title: 'Modifier le passage',
passageRepository: _passageRepository,
userRepository: _userRepository,
operationRepository: operationRepository,
onSuccess: () {
debugPrint('Dialog fermé avec succès');
// Recharger les données après modification
_loadPassages();
},
),
);
// Flux conditionnel selon le type de passage
if (passageType == 2) {
// Type 2 ("À finaliser") : Ouvrir directement le formulaire de modification
debugPrint('Passage type 2 - Ouverture directe du formulaire');
_showEditDialog(context, passageModel);
} else {
// Autres types : Afficher d'abord les détails avec option de modification
debugPrint('Passage type $passageType - Affichage des détails d\'abord');
_showDetailsDialogWithEditOption(context, passage, passageModel);
}
debugPrint('=== FIN _openPassageEditDialog ===');
} catch (e, stackTrace) {
debugPrint('=== ERREUR _openPassageEditDialog ===');
@@ -797,13 +1098,35 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de l\'ouverture du formulaire: $e'),
content: Text('Erreur lors de l\'ouverture: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
// Méthode extraite pour ouvrir le dialog de modification
void _showEditDialog(BuildContext context, PassageModel passageModel) {
debugPrint('Ouverture du formulaire de modification pour le passage ${passageModel.id}');
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => PassageFormDialog(
passage: passageModel,
title: 'Modifier le passage',
passageRepository: _passageRepository,
userRepository: _userRepository,
operationRepository: operationRepository,
onSuccess: () {
debugPrint('Dialog fermé avec succès');
// Recharger les données après modification
_loadPassages();
},
),
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
@@ -1334,6 +1657,209 @@ class _AdminHistoryPageState extends State<AdminHistoryPage> {
);
}
// Afficher le dialog de confirmation de suppression
void _showDeleteConfirmationDialog(Map<String, dynamic> passage) {
final TextEditingController confirmController = TextEditingController();
// Récupérer l'ID du passage et trouver le PassageModel original
final int passageId = passage['id'] as int;
final PassageModel? passageModel =
_originalPassages.where((p) => p.id == passageId).firstOrNull;
if (passageModel == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Impossible de trouver le passage'),
backgroundColor: Colors.red,
),
);
return;
}
final String streetNumber = passageModel.numero ?? '';
final String fullAddress = '${passageModel.numero ?? ''} ${passageModel.rueBis ?? ''} ${passageModel.rue ?? ''}'.trim();
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Row(
children: [
Icon(Icons.warning, color: Colors.red, size: 28),
SizedBox(width: 8),
Text('Confirmation de suppression'),
],
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'ATTENTION : Cette action est irréversible !',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red,
fontSize: 16,
),
),
const SizedBox(height: 16),
Text(
'Vous êtes sur le point de supprimer définitivement le passage :',
style: TextStyle(color: Colors.grey[800]),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fullAddress.isEmpty ? 'Adresse inconnue' : fullAddress,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
const SizedBox(height: 4),
if (passage['user'] != null)
Text(
'Collecteur: ${passage['user']}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
if (passage['date'] != null)
Text(
'Date: ${_formatDate(passage['date'] as DateTime)}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
const SizedBox(height: 20),
const Text(
'Pour confirmer la suppression, veuillez saisir le numéro de rue de ce passage :',
style: TextStyle(fontWeight: FontWeight.w500),
),
const SizedBox(height: 12),
TextField(
controller: confirmController,
decoration: InputDecoration(
labelText: 'Numéro de rue',
hintText: streetNumber.isNotEmpty ? 'Ex: $streetNumber' : 'Saisir le numéro',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.home),
),
keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.characters,
),
],
),
),
actions: [
TextButton(
onPressed: () {
confirmController.dispose();
Navigator.of(dialogContext).pop();
},
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () async {
// Vérifier que le numéro saisi correspond
final enteredNumber = confirmController.text.trim();
if (enteredNumber.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez saisir le numéro de rue'),
backgroundColor: Colors.orange,
),
);
return;
}
if (streetNumber.isNotEmpty && enteredNumber.toUpperCase() != streetNumber.toUpperCase()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Le numéro de rue ne correspond pas'),
backgroundColor: Colors.red,
),
);
return;
}
// Fermer le dialog
confirmController.dispose();
Navigator.of(dialogContext).pop();
// Effectuer la suppression
await _deletePassage(passageModel);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Supprimer définitivement'),
),
],
);
},
);
}
// Supprimer un passage
Future<void> _deletePassage(PassageModel passage) async {
try {
// Appeler le repository pour supprimer via l'API
final success = await _passageRepository.deletePassageViaApi(passage.id);
if (success && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Passage supprimé avec succès'),
backgroundColor: Colors.green,
),
);
// Pas besoin de recharger, le ValueListenableBuilder
// se rafraîchira automatiquement après la suppression dans Hive
} else if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Erreur lors de la suppression du passage'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
debugPrint('Erreur suppression passage: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
// Formater une date
String _formatDate(DateTime date) {
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
}
// Construction du filtre par mode de règlement
Widget _buildPaymentFilter(ThemeData theme) {
return Column(

View File

@@ -20,6 +20,7 @@ import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/services/current_user_service.dart';
import 'package:geosector_app/core/repositories/operation_repository.dart';
import 'package:geosector_app/core/services/data_loading_service.dart';
import 'package:geosector_app/presentation/widgets/passage_map_dialog.dart';
class AdminMapPage extends StatefulWidget {
const AdminMapPage({super.key});
@@ -991,148 +992,16 @@ class _AdminMapPageState extends State<AdminMapPage> {
// Afficher les informations d'un passage lorsqu'on clique dessus
void _showPassageInfo(Map<String, dynamic> passage) {
final PassageModel passageModel = passage['model'] as PassageModel;
final int type = passageModel.fkType;
// Construire l'adresse complète
final String adresse = '${passageModel.numero}, ${passageModel.rueBis} ${passageModel.rue}';
// Informations sur l'étage, l'appartement et la résidence (si habitat = 2)
String? etageInfo;
String? apptInfo;
String? residenceInfo;
if (passageModel.fkHabitat == 2) {
if (passageModel.niveau.isNotEmpty) {
etageInfo = 'Etage ${passageModel.niveau}';
}
if (passageModel.appt.isNotEmpty) {
apptInfo = 'appt. ${passageModel.appt}';
}
if (passageModel.residence.isNotEmpty) {
residenceInfo = passageModel.residence;
}
}
// Formater la date (uniquement si le type n'est pas 2 et si la date existe)
String dateInfo = '';
if (type != 2 && passageModel.passedAt != null) {
dateInfo = 'Date: ${_formatDate(passageModel.passedAt!)}';
}
// Récupérer le nom du passage (si le type n'est pas 6 - Maison vide)
String? nomInfo;
if (type != 6 && passageModel.name.isNotEmpty) {
nomInfo = passageModel.name;
}
// Récupérer les informations de règlement si le type est 1 (Effectué) ou 5 (Lot)
Widget? reglementInfo;
if (type == 1 || type == 5) {
final int typeReglementId = passageModel.fkTypeReglement;
final String montant = passageModel.montant;
// Récupérer les informations du type de règlement
if (AppKeys.typesReglements.containsKey(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;
reglementInfo = Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
Icon(iconData, color: couleur, size: 20),
const SizedBox(width: 8),
Text('$titre: $montant', style: TextStyle(color: couleur, fontWeight: FontWeight.bold)),
],
),
);
}
}
// Afficher une bulle d'information
showDialog(
context: context,
builder: (context) => AlertDialog(
contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Afficher en premier si le passage n'est pas affecté à un secteur
if (passageModel.fkSector == null) ...[
Container(
padding: const EdgeInsets.all(8),
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
border: Border.all(color: Colors.red, width: 1),
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
const Icon(Icons.warning, color: Colors.red, size: 20),
const SizedBox(width: 8),
const Expanded(
child: Text(
'Ce passage n\'est plus affecté à un secteur',
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
),
),
],
),
),
],
Text('Adresse: $adresse'),
if (residenceInfo != null) ...[const SizedBox(height: 4), Text(residenceInfo)],
if (etageInfo != null) ...[const SizedBox(height: 4), Text(etageInfo)],
if (apptInfo != null) ...[const SizedBox(height: 4), Text(apptInfo)],
if (dateInfo.isNotEmpty) ...[const SizedBox(height: 8), Text(dateInfo)],
if (nomInfo != null) ...[const SizedBox(height: 8), Text('Nom: $nomInfo')],
if (reglementInfo != null) reglementInfo,
],
),
actionsPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
// Bouton d'édition
IconButton(
onPressed: () {
Navigator.of(context).pop();
// Logique pour éditer le passage
debugPrint('Éditer le passage ${passageModel.id}');
},
icon: const Icon(Icons.edit),
color: Colors.blue,
tooltip: 'Modifier',
),
// Bouton de suppression
IconButton(
onPressed: () {
Navigator.of(context).pop();
// Logique pour supprimer le passage
debugPrint('Supprimer le passage ${passageModel.id}');
},
icon: const Icon(Icons.delete),
color: Colors.red,
tooltip: 'Supprimer',
),
],
),
// Bouton de fermeture
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
],
),
],
builder: (context) => PassageMapDialog(
passage: passageModel,
isAdmin: true,
onDeleted: () {
// Recharger les passages après suppression
_loadPassages();
},
),
);
}

View File

@@ -176,76 +176,92 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
// Construction de la liste des derniers passages
Widget _buildRecentPassages(BuildContext context, ThemeData theme) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 0.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Derniers passages',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
// Utilisation directe du widget PassagesListWidget sans Card wrapper
return ValueListenableBuilder(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
final recentPassages = _getRecentPassages(passagesBox);
// Debug : afficher le nombre de passages récupérés
debugPrint('UserDashboardHomePage: ${recentPassages.length} passages récents récupérés');
if (recentPassages.isEmpty) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: const Padding(
padding: EdgeInsets.all(32.0),
child: Center(
child: Text(
'Aucun passage récent',
style: TextStyle(
color: Colors.grey,
fontSize: 14,
),
),
TextButton(
onPressed: () {
// Naviguer vers la page d'historique
},
child: const Text('Voir tout'),
),
],
),
),
),
// Utilisation du widget commun PassagesListWidget avec ValueListenableBuilder
ValueListenableBuilder(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
final recentPassages = _getRecentPassages(passagesBox);
);
}
return PassagesListWidget(
passages: recentPassages,
showFilters: false,
showSearch: false,
showActions: true,
maxPassages: 10,
excludePassageTypes: const [2],
filterByUserId: userRepository.getCurrentUser()?.id,
periodFilter: 'last15',
onPassageSelected: (passage) {
debugPrint('Passage sélectionné: ${passage['id']}');
},
onDetailsView: (passage) {
debugPrint('Affichage des détails: ${passage['id']}');
},
onPassageEdit: (passage) {
debugPrint('Modification du passage: ${passage['id']}');
},
onReceiptView: (passage) {
debugPrint('Affichage du reçu pour le passage: ${passage['id']}');
},
);
// Utiliser une hauteur fixe pour le widget dans le dashboard
return SizedBox(
height: 450, // Hauteur légèrement augmentée pour compenser l'absence de Card
child: PassagesListWidget(
passages: recentPassages,
showFilters: false,
showSearch: false,
showActions: true,
maxPassages: 20,
// Ne pas appliquer de filtres supplémentaires car les passages
// sont déjà filtrés dans _getRecentPassages
excludePassageTypes: null, // Pas de filtre, déjà géré dans _getRecentPassages
filterByUserId: null, // Pas de filtre, déjà géré dans _getRecentPassages
periodFilter: null, // Pas de filtre de période
// Le widget gère maintenant le flux conditionnel par défaut
onPassageSelected: null,
onDetailsView: (passage) {
debugPrint('Affichage des détails: ${passage['id']}');
},
onPassageEdit: (passage) {
debugPrint('Modification du passage: ${passage['id']}');
},
onReceiptView: (passage) {
debugPrint('Affichage du reçu pour le passage: ${passage['id']}');
},
onPassageDelete: (passage) {
// Pas besoin de faire quoi que ce soit ici
// Le ValueListenableBuilder se rafraîchira automatiquement
// après la suppression dans Hive via le repository
},
),
],
),
);
},
);
}
/// Récupère les passages récents pour la liste
List<Map<String, dynamic>> _getRecentPassages(Box<PassageModel> passagesBox) {
final allPassages = passagesBox.values.where((p) => p.passedAt != null).toList();
final currentUserId = userRepository.getCurrentUser()?.id;
// Filtrer les passages :
// - Avoir une date passedAt
// - Exclure le type 2 ("À finaliser")
// - Appartenir à l'utilisateur courant
final allPassages = passagesBox.values.where((p) {
if (p.passedAt == null) return false;
if (p.fkType == 2) return false; // Exclure les passages "À finaliser"
if (currentUserId != null && p.fkUser != currentUserId) return false; // Filtrer par utilisateur
return true;
}).toList();
// Trier par date décroissante
allPassages.sort((a, b) => b.passedAt!.compareTo(a.passedAt!));
// Limiter aux 10 passages les plus récents
final recentPassagesModels = allPassages.take(10).toList();
// Limiter aux 20 passages les plus récents
final recentPassagesModels = allPassages.take(20).toList();
// Convertir les modèles de passage au format attendu par le widget PassagesListWidget
return recentPassagesModels.map((passage) {
@@ -278,6 +294,7 @@ Widget _buildCombinedPaymentsCard(bool isDesktop) {
'hasReceipt': passage.nomRecu.isNotEmpty,
'hasError': passage.emailErreur.isNotEmpty,
'fkUser': passage.fkUser,
'isOwnedByCurrentUser': passage.fkUser == userRepository.getCurrentUser()?.id, // Ajout du champ pour le widget
};
}).toList();
}

View File

@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/presentation/widgets/dashboard_layout.dart';
import 'package:geosector_app/presentation/widgets/passages/passage_form.dart';
import 'package:geosector_app/presentation/widgets/badged_navigation_destination.dart';
// Import des pages utilisateur
@@ -109,7 +108,6 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
label: 'Accès restreint',
),
],
showNewPassageButton: false,
body: _buildNoOperationMessage(context),
);
}
@@ -128,7 +126,6 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
label: 'Accès restreint',
),
],
showNewPassageButton: false,
body: _buildNoSectorMessage(context),
);
}
@@ -176,7 +173,6 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
label: 'Terrain',
),
],
onNewPassagePressed: () => _showPassageForm(context),
body: _pages[_selectedIndex],
);
}
@@ -282,95 +278,4 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
}
// Affiche le formulaire de passage
void _showPassageForm(BuildContext context) {
final theme = Theme.of(context);
showDialog(
context: context,
builder: (context) => Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
constraints: const BoxConstraints(
maxWidth: 600,
maxHeight: 700,
),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête de la modale
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Nouveau passage',
style: theme.textTheme.headlineSmall?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
],
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
// Formulaire de passage
Expanded(
child: SingleChildScrollView(
child: PassageForm(
onSubmit: (formData) {
// Traiter les données du formulaire
_handlePassageSubmission(context, formData);
},
),
),
),
],
),
),
),
);
}
// Traiter la soumission du formulaire de passage
void _handlePassageSubmission(
BuildContext context, Map<String, dynamic> formData) {
// Fermer la modale
Navigator.of(context).pop();
// Ici vous pouvez traiter les données du formulaire
// Par exemple, les envoyer au repository ou à un service
// Pour l'instant, afficher un message de succès
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('Passage enregistré avec succès pour ${formData['adresse']}'),
backgroundColor: Theme.of(context).colorScheme.primary,
behavior: SnackBarBehavior.floating,
),
);
// TODO: Intégrer avec votre logique métier
// Exemple :
// try {
// await passageRepository.createPassage(formData);
// // Rafraîchir les données si nécessaire
// } catch (e) {
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(
// content: Text('Erreur lors de l\'enregistrement: $e'),
// backgroundColor: Theme.of(context).colorScheme.error,
// ),
// );
// }
}
}

View File

@@ -14,8 +14,10 @@ import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/services/current_amicale_service.dart';
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
import 'package:geosector_app/app.dart';
import 'package:sensors_plus/sensors_plus.dart';
import 'package:geosector_app/core/utils/api_exception.dart';
class UserFieldModePage extends StatefulWidget {
const UserFieldModePage({super.key});
@@ -372,6 +374,164 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
),
);
}
// Vérifier si l'amicale autorise la suppression des passages
bool _canDeletePassages() {
try {
final amicale = CurrentAmicaleService.instance.currentAmicale;
if (amicale != null) {
return amicale.chkUserDeletePass == true;
}
} catch (e) {
debugPrint('Erreur lors de la vérification des permissions de suppression: $e');
}
return false;
}
// Afficher le dialog de confirmation de suppression
void _showDeleteConfirmationDialog(PassageModel passage) {
final TextEditingController confirmController = TextEditingController();
final String streetNumber = passage.numero ?? '';
final String fullAddress = '${passage.numero ?? ''} ${passage.rueBis ?? ''} ${passage.rue ?? ''}'.trim();
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Row(
children: [
Icon(Icons.warning, color: Colors.red, size: 28),
SizedBox(width: 8),
Text('Confirmation de suppression'),
],
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'ATTENTION : Cette action est irréversible !',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red,
fontSize: 16,
),
),
const SizedBox(height: 16),
Text(
'Vous êtes sur le point de supprimer définitivement le passage :',
style: TextStyle(color: Colors.grey[800]),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: Text(
fullAddress.isEmpty ? 'Adresse inconnue' : fullAddress,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
),
const SizedBox(height: 20),
const Text(
'Pour confirmer la suppression, veuillez saisir le numéro de rue de ce passage :',
style: TextStyle(fontWeight: FontWeight.w500),
),
const SizedBox(height: 12),
TextField(
controller: confirmController,
decoration: InputDecoration(
labelText: 'Numéro de rue',
hintText: streetNumber.isNotEmpty ? 'Ex: $streetNumber' : 'Saisir le numéro',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.home),
),
keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.characters,
),
],
),
),
actions: [
TextButton(
onPressed: () {
confirmController.dispose();
Navigator.of(dialogContext).pop();
},
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () async {
// Vérifier que le numéro saisi correspond
final enteredNumber = confirmController.text.trim();
if (enteredNumber.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Veuillez saisir le numéro de rue'),
backgroundColor: Colors.orange,
),
);
return;
}
if (streetNumber.isNotEmpty && enteredNumber.toUpperCase() != streetNumber.toUpperCase()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Le numéro de rue ne correspond pas'),
backgroundColor: Colors.red,
),
);
return;
}
// Fermer le dialog
confirmController.dispose();
Navigator.of(dialogContext).pop();
// Effectuer la suppression
await _deletePassage(passage);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Supprimer définitivement'),
),
],
);
},
);
}
// Supprimer un passage
Future<void> _deletePassage(PassageModel passage) async {
try {
// Appeler le repository pour supprimer via l'API
final success = await passageRepository.deletePassageViaApi(passage.id);
if (success && mounted) {
ApiException.showSuccess(context, 'Passage supprimé avec succès');
// Rafraîchir la liste des passages
_updateNearbyPassages();
} else if (mounted) {
ApiException.showError(context, Exception('Erreur lors de la suppression'));
}
} catch (e) {
debugPrint('Erreur suppression passage: $e');
if (mounted) {
ApiException.showError(context, e);
}
}
}
@override
void dispose() {
@@ -854,149 +1014,112 @@ class _UserFieldModePageState extends State<UserFieldModePage> with TickerProvid
}).toList();
}
List<PassageModel> _getFilteredPassages() {
if (_searchQuery.isEmpty) {
return _nearbyPassages;
}
return _nearbyPassages.where((passage) {
// Construire l'adresse à partir des champs disponibles
final address = '${passage.numero ?? ''} ${passage.rueBis ?? ''} ${passage.rue ?? ''}'.trim().toLowerCase();
return address.contains(_searchQuery);
List<Map<String, dynamic>> _getFilteredPassages() {
// Filtrer d'abord par recherche si nécessaire
List<PassageModel> filtered = _searchQuery.isEmpty
? _nearbyPassages
: _nearbyPassages.where((passage) {
final address = '${passage.numero ?? ''} ${passage.rueBis ?? ''} ${passage.rue ?? ''}'.trim().toLowerCase();
return address.contains(_searchQuery);
}).toList();
// Convertir au format attendu par PassagesListWidget avec distance
return filtered.map((passage) {
// Calculer la distance
final double lat = double.tryParse(passage.gpsLat ?? '0') ?? 0;
final double lng = double.tryParse(passage.gpsLng ?? '0') ?? 0;
final distance = _currentPosition != null
? _calculateDistance(
_currentPosition!.latitude,
_currentPosition!.longitude,
lat,
lng,
)
: 0.0;
// Construire l'adresse complète
final String address = '${passage.numero ?? ''} ${passage.rueBis ?? ''} ${passage.rue ?? ''}'.trim();
// Convertir le montant
double amount = 0.0;
try {
if (passage.montant.isNotEmpty) {
String montantStr = passage.montant.replaceAll(',', '.');
amount = double.tryParse(montantStr) ?? 0.0;
}
} catch (e) {
// Ignorer les erreurs de conversion
}
return {
'id': passage.id,
'address': address.isEmpty ? 'Adresse inconnue' : address,
'amount': amount,
'date': passage.passedAt ?? DateTime.now(),
'type': passage.fkType,
'payment': passage.fkTypeReglement,
'name': passage.name,
'notes': passage.remarque,
'hasReceipt': passage.nomRecu.isNotEmpty,
'hasError': passage.emailErreur.isNotEmpty,
'fkUser': passage.fkUser,
'distance': distance, // Ajouter la distance pour le tri et l'affichage
'nbPassages': passage.nbPassages, // Pour la couleur de l'indicateur
'isOwnedByCurrentUser': passage.fkUser == userRepository.getCurrentUser()?.id, // Ajout du champ pour le widget
// Garder les données originales pour l'édition
'numero': passage.numero,
'rueBis': passage.rueBis,
'rue': passage.rue,
'ville': passage.ville,
};
}).toList();
}
Widget _buildPassagesList() {
final filteredPassages = _getFilteredPassages();
if (filteredPassages.isEmpty) {
return Container(
color: Colors.white,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
_searchQuery.isNotEmpty
? 'Aucun passage trouvé pour "$_searchQuery"'
: 'Aucun passage à proximité',
style: TextStyle(color: Colors.grey[600]),
),
],
),
),
);
}
return Container(
color: Colors.white,
child: ListView.separated(
padding: const EdgeInsets.only(bottom: 20),
itemCount: filteredPassages.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final passage = filteredPassages[index];
// Convertir les coordonnées GPS string en double
final double lat = double.tryParse(passage.gpsLat ?? '0') ?? 0;
final double lng = double.tryParse(passage.gpsLng ?? '0') ?? 0;
final distance = _currentPosition != null
? _calculateDistance(
_currentPosition!.latitude,
_currentPosition!.longitude,
lat,
lng,
)
: 0.0;
// Formater la distance
String distanceText;
if (distance < 1000) {
distanceText = '${distance.toStringAsFixed(0)} m';
} else {
distanceText = '${(distance / 1000).toStringAsFixed(1)} km';
}
// Couleur selon nbPassages
Color indicatorColor;
if (passage.nbPassages == 0) {
indicatorColor = Colors.grey[400]!;
} else if (passage.nbPassages == 1) {
indicatorColor = const Color(0xFFF7A278);
} else {
indicatorColor = const Color(0xFFE65100);
}
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: indicatorColor,
border: Border.all(color: const Color(0xFFF7A278), width: 2), // couleur2: Orange
),
),
title: Text(
'${passage.numero ?? ''} ${passage.rueBis ?? ''} ${passage.rue ?? ''}'.trim().isEmpty
? 'Adresse inconnue'
: '${passage.numero ?? ''} ${passage.rueBis ?? ''} ${passage.rue ?? ''}'.trim(),
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 15,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Row(
children: [
Icon(Icons.navigation, size: 14, color: Colors.green[600]),
const SizedBox(width: 4),
Text(
distanceText,
style: TextStyle(
color: Colors.green[700],
fontWeight: FontWeight.w500,
fontSize: 13,
),
),
if (passage.name != null && passage.name!.isNotEmpty) ...[
const SizedBox(width: 12),
Expanded(
child: Text(
passage.name!,
style: TextStyle(
color: Colors.grey[600],
fontSize: 13,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
],
),
trailing: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green[50],
shape: BoxShape.circle,
),
child: Icon(
Icons.arrow_forward_ios,
size: 16,
color: Colors.green[700],
),
),
onTap: () {
HapticFeedback.lightImpact();
_openPassageForm(passage);
child: PassagesListWidget(
passages: filteredPassages,
showFilters: false, // Pas de filtres, juste la liste
showSearch: false, // La recherche est déjà dans l'interface
showActions: true,
sortBy: 'distance', // Tri par distance pour le mode terrain
excludePassageTypes: const [], // Afficher tous les types (notamment le type 2)
showAddButton: true, // Activer le bouton de création
// Le widget gère maintenant le flux conditionnel par défaut
onPassageSelected: null,
onAddPassage: () async {
// Ouvrir le dialogue de création de passage
await showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return PassageFormDialog(
title: 'Nouveau passage',
passageRepository: passageRepository,
userRepository: userRepository,
operationRepository: operationRepository,
onSuccess: () {
// Le widget se rafraîchira automatiquement via ValueListenableBuilder
},
);
},
);
},
onPassageDelete: _canDeletePassages()
? (passage) {
// Retrouver le PassageModel original pour la suppression
final passageId = passage['id'] as int;
final originalPassage = _nearbyPassages.firstWhere(
(p) => p.id == passageId,
orElse: () => _nearbyPassages.first,
);
_showDeleteConfirmationDialog(originalPassage);
}
: null,
),
);
}

View File

@@ -1,7 +1,9 @@
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
// Pour accéder aux instances globales
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
@@ -12,6 +14,14 @@ class UserHistoryPage extends StatefulWidget {
State<UserHistoryPage> createState() => _UserHistoryPageState();
}
// Enum pour gérer les types de tri
enum PassageSortType {
dateDesc, // Plus récent en premier (défaut)
dateAsc, // Plus ancien en premier
addressAsc, // Adresse A-Z
addressDesc, // Adresse Z-A
}
class _UserHistoryPageState extends State<UserHistoryPage> {
// Liste qui contiendra les passages convertis
List<Map<String, dynamic>> _convertedPassages = [];
@@ -19,6 +29,13 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
// Variables pour indiquer l'état de chargement
bool _isLoading = true;
String _errorMessage = '';
// Statistiques pour l'affichage
int _totalSectors = 0;
int _sharedMembersCount = 0;
// État du tri actuel
PassageSortType _currentSort = PassageSortType.dateDesc;
@override
void initState() {
@@ -42,21 +59,10 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
debugPrint('Nombre total de passages dans la box: ${allPassages.length}');
// Filtrer pour exclure les passages de type 2
List<PassageModel> filtered = [];
for (var passage in allPassages) {
try {
if (passage.fkType != 2) {
filtered.add(passage);
}
} catch (e) {
debugPrint('Erreur lors du filtrage du passage: $e');
// Si nous ne pouvons pas accéder à fkType, ne pas ajouter ce passage
}
}
// Ne plus filtrer les passages de type 2 - laisser le widget gérer le filtrage
List<PassageModel> filtered = allPassages;
debugPrint(
'Nombre de passages après filtrage (fkType != 2): ${filtered.length}');
debugPrint('Nombre total de passages disponibles: ${filtered.length}');
// Afficher la distribution des types de passages pour le débogage
final Map<int, int> typeCount = {};
@@ -156,9 +162,34 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
debugPrint('Premier passage: ${firstDate.toString()}');
debugPrint('Dernier passage: ${lastDate.toString()}');
}
// Calculer le nombre de secteurs uniques
final Set<int> uniqueSectors = {};
for (var passage in filtered) {
if (passage.fkSector != null && passage.fkSector! > 0) {
uniqueSectors.add(passage.fkSector!);
}
}
// Compter les membres partagés (autres membres dans la même amicale)
int sharedMembers = 0;
try {
// Utiliser l'instance globale définie dans app.dart
final currentUserId = userRepository.getCurrentUser()?.id;
final allMembers = membreRepository.membres; // Utiliser la propriété membres
// Compter les membres autres que l'utilisateur courant
sharedMembers = allMembers.where((membre) => membre.id != currentUserId).length;
debugPrint('Nombre de membres partagés: $sharedMembers');
} catch (e) {
debugPrint('Erreur lors du comptage des membres: $e');
}
setState(() {
_convertedPassages = passagesMap;
_totalSectors = uniqueSectors.length;
_sharedMembersCount = sharedMembers;
_isLoading = false;
});
} catch (e) {
@@ -207,13 +238,13 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
int type;
try {
type = passage.fkType;
// Si le type n'est pas dans les types connus, utiliser 0 comme valeur par défaut
// Si le type n'est pas dans les types connus, utiliser 1 comme valeur par défaut
if (!AppKeys.typesPassages.containsKey(type)) {
type = 0; // Type inconnu
type = 1; // Type 1 par défaut (Effectué)
}
} catch (e) {
debugPrint('Erreur lors de la récupération du type: $e');
type = 0;
type = 1; // Type 1 par défaut
}
// Récupérer le type de règlement avec gestion d'erreur
@@ -265,7 +296,7 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
'Conversion passage ID: ${passage.id}, Type: $type, Date: $date');
return {
'id': passage.id.toString(),
'id': passage.id, // Garder l'ID comme int, pas besoin de toString()
'address': address,
'amount': amount,
'date': date,
@@ -276,6 +307,11 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
'hasReceipt': hasReceipt,
'hasError': hasError,
'fkUser': passage.fkUser, // Ajouter l'ID de l'utilisateur
'isOwnedByCurrentUser': passage.fkUser == userRepository.getCurrentUser()?.id, // Ajout du champ pour le widget
// Ajouter les composants de l'adresse pour le tri
'rue': passage.rue,
'numero': passage.numero,
'rueBis': passage.rueBis,
};
} catch (e) {
debugPrint('ERREUR CRITIQUE lors de la conversion du passage: $e');
@@ -289,17 +325,105 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
'address': 'Adresse non disponible',
'amount': 0.0,
'date': DateTime.now(),
'type': 0,
'payment': 0,
'type': 1, // Type 1 par défaut au lieu de 0
'payment': 1, // Payment 1 par défaut au lieu de 0
'name': 'Nom non disponible',
'notes': '',
'hasReceipt': false,
'hasError': true,
'fkUser': currentUserId, // Ajouter l'ID de l'utilisateur courant
// Composants de l'adresse pour le tri
'rue': '',
'numero': '',
'rueBis': '',
};
}
}
// Méthode pour trier les passages selon le type de tri sélectionné
List<Map<String, dynamic>> _sortPassages(List<Map<String, dynamic>> passages) {
final sortedPassages = List<Map<String, dynamic>>.from(passages);
switch (_currentSort) {
case PassageSortType.dateDesc:
sortedPassages.sort((a, b) {
try {
return (b['date'] as DateTime).compareTo(a['date'] as DateTime);
} catch (e) {
return 0;
}
});
break;
case PassageSortType.dateAsc:
sortedPassages.sort((a, b) {
try {
return (a['date'] as DateTime).compareTo(b['date'] as DateTime);
} catch (e) {
return 0;
}
});
break;
case PassageSortType.addressAsc:
sortedPassages.sort((a, b) {
try {
// Tri intelligent par rue, numéro (numérique), rueBis
final String rueA = a['rue'] ?? '';
final String rueB = b['rue'] ?? '';
final String numeroA = a['numero'] ?? '';
final String numeroB = b['numero'] ?? '';
final String rueBisA = a['rueBis'] ?? '';
final String rueBisB = b['rueBis'] ?? '';
// D'abord comparer les rues
int rueCompare = rueA.toLowerCase().compareTo(rueB.toLowerCase());
if (rueCompare != 0) return rueCompare;
// Si les rues sont identiques, comparer les numéros (numériquement)
int numA = int.tryParse(numeroA) ?? 0;
int numB = int.tryParse(numeroB) ?? 0;
int numCompare = numA.compareTo(numB);
if (numCompare != 0) return numCompare;
// Si les numéros sont identiques, comparer les rueBis
return rueBisA.toLowerCase().compareTo(rueBisB.toLowerCase());
} catch (e) {
return 0;
}
});
break;
case PassageSortType.addressDesc:
sortedPassages.sort((a, b) {
try {
// Tri intelligent inversé par rue, numéro (numérique), rueBis
final String rueA = a['rue'] ?? '';
final String rueB = b['rue'] ?? '';
final String numeroA = a['numero'] ?? '';
final String numeroB = b['numero'] ?? '';
final String rueBisA = a['rueBis'] ?? '';
final String rueBisB = b['rueBis'] ?? '';
// D'abord comparer les rues (inversé)
int rueCompare = rueB.toLowerCase().compareTo(rueA.toLowerCase());
if (rueCompare != 0) return rueCompare;
// Si les rues sont identiques, comparer les numéros (inversé numériquement)
int numA = int.tryParse(numeroA) ?? 0;
int numB = int.tryParse(numeroB) ?? 0;
int numCompare = numB.compareTo(numA);
if (numCompare != 0) return numCompare;
// Si les numéros sont identiques, comparer les rueBis (inversé)
return rueBisB.toLowerCase().compareTo(rueBisA.toLowerCase());
} catch (e) {
return 0;
}
});
break;
}
return sortedPassages;
}
// Construire l'adresse complète à partir des composants
String _buildFullAddress(PassageModel passage) {
final List<String> addressParts = [];
@@ -457,20 +581,45 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
// En-tête avec bouton de rafraîchissement
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Historique des passages',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadPassages,
tooltip: 'Rafraîchir',
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_isLoading
? 'Historique des passages'
: 'Historique des ${_convertedPassages.length} passages${_totalSectors > 0 ? ' ($_totalSectors secteur${_totalSectors > 1 ? 's' : ''})' : ''}',
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
if (!_isLoading && _sharedMembersCount > 0)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
'Partagés avec $_sharedMembersCount membre${_sharedMembersCount > 1 ? 's' : ''}',
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
fontStyle: FontStyle.italic,
),
),
),
],
),
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadPassages,
tooltip: 'Rafraîchir',
),
],
),
],
),
@@ -510,61 +659,159 @@ class _UserHistoryPageState extends State<UserHistoryPage> {
)
// Utilisation du widget PassagesListWidget pour afficher la liste des passages
else
Column(
children: [
// Stat rapide pour l'utilisateur
if (_convertedPassages.isNotEmpty)
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${_convertedPassages.length} passages au total (${_convertedPassages.where((p) => (p['date'] as DateTime).isAfter(DateTime(2024, 12, 13))).length} de décembre 2024)',
style: TextStyle(
fontStyle: FontStyle.italic,
color: theme.colorScheme.primary),
Expanded(
child: Container(
color: Colors.transparent,
child: Column(
children: [
// Widget de liste des passages avec ValueListenableBuilder
Expanded(
child: ValueListenableBuilder(
valueListenable: Hive.box<PassageModel>(AppKeys.passagesBoxName).listenable(),
builder: (context, Box<PassageModel> passagesBox, child) {
// Reconvertir les passages à chaque changement
final List<PassageModel> allPassages = passagesBox.values.toList();
// Appliquer le même filtrage et conversion
List<Map<String, dynamic>> passagesMap = [];
for (var passage in allPassages) {
try {
final Map<String, dynamic> passageMap = _convertPassageModelToMap(passage);
passagesMap.add(passageMap);
} catch (e) {
debugPrint('Erreur lors de la conversion du passage en map: $e');
}
}
// Appliquer le tri sélectionné
passagesMap = _sortPassages(passagesMap);
return PassagesListWidget(
showAddButton: true, // Activer le bouton de création
onAddPassage: () async {
// Ouvrir le dialogue de création de passage
await showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return PassageFormDialog(
title: 'Nouveau passage',
passageRepository: passageRepository,
userRepository: userRepository,
operationRepository: operationRepository,
onSuccess: () {
// Le widget se rafraîchira automatiquement via ValueListenableBuilder
},
);
},
);
},
sortingButtons: Row(
children: [
// Bouton tri par date avec icône calendrier
IconButton(
icon: Icon(
Icons.calendar_today,
size: 20,
color: _currentSort == PassageSortType.dateDesc ||
_currentSort == PassageSortType.dateAsc
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withOpacity(0.6),
),
tooltip: _currentSort == PassageSortType.dateAsc
? 'Tri par date (ancien en premier)'
: 'Tri par date (récent en premier)',
onPressed: () {
setState(() {
if (_currentSort == PassageSortType.dateDesc) {
_currentSort = PassageSortType.dateAsc;
} else {
_currentSort = PassageSortType.dateDesc;
}
});
},
),
// Indicateur de direction pour la date
if (_currentSort == PassageSortType.dateDesc ||
_currentSort == PassageSortType.dateAsc)
Icon(
_currentSort == PassageSortType.dateAsc
? Icons.arrow_upward
: Icons.arrow_downward,
size: 14,
color: theme.colorScheme.primary,
),
const SizedBox(width: 4),
// Bouton tri par adresse avec icône maison
IconButton(
icon: Icon(
Icons.home,
size: 20,
color: _currentSort == PassageSortType.addressDesc ||
_currentSort == PassageSortType.addressAsc
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withOpacity(0.6),
),
tooltip: _currentSort == PassageSortType.addressAsc
? 'Tri par adresse (A-Z)'
: 'Tri par adresse (Z-A)',
onPressed: () {
setState(() {
if (_currentSort == PassageSortType.addressAsc) {
_currentSort = PassageSortType.addressDesc;
} else {
_currentSort = PassageSortType.addressAsc;
}
});
},
),
// Indicateur de direction pour l'adresse
if (_currentSort == PassageSortType.addressDesc ||
_currentSort == PassageSortType.addressAsc)
Icon(
_currentSort == PassageSortType.addressAsc
? Icons.arrow_upward
: Icons.arrow_downward,
size: 14,
color: theme.colorScheme.primary,
),
],
),
passages: passagesMap,
showFilters: true,
showSearch: true,
showActions: true,
initialSearchQuery: _searchQuery,
initialTypeFilter: 'Tous',
initialPaymentFilter: 'Tous',
excludePassageTypes: const [],
filterByUserId: userRepository.getCurrentUser()?.id,
key: const ValueKey('user_passages_list'),
// Le widget gère maintenant le flux conditionnel par défaut
onPassageSelected: null,
onDetailsView: (passage) {
debugPrint('Affichage des détails: ${passage['id']}');
_showPassageDetails(passage);
},
onPassageEdit: (passage) {
debugPrint('Modification du passage: ${passage['id']}');
_editPassage(passage);
},
onReceiptView: (passage) {
debugPrint('Affichage du reçu pour le passage: ${passage['id']}');
_showReceipt(passage);
},
onPassageDelete: (passage) {
// Pas besoin de recharger, le ValueListenableBuilder
// se rafraîchira automatiquement après la suppression
},
);
},
),
),
),
// Widget de liste des passages
Expanded(
child: PassagesListWidget(
passages: _convertedPassages,
showFilters: true,
showSearch: true,
showActions: true,
initialSearchQuery: _searchQuery,
initialTypeFilter:
'Tous', // Toujours commencer avec 'Tous' pour voir tous les types
initialPaymentFilter: 'Tous',
// Exclure les passages de type 2 (À finaliser)
excludePassageTypes: const [2],
// Filtrer par utilisateur courant
filterByUserId: userRepository.getCurrentUser()?.id,
// Désactiver les filtres de date implicites
key: ValueKey(
'passages_list_${DateTime.now().millisecondsSinceEpoch}'),
onPassageSelected: (passage) {
// Action lors de la sélection d'un passage
debugPrint('Passage sélectionné: ${passage['id']}');
_showPassageDetails(passage);
},
onDetailsView: (passage) {
// Action lors de l'affichage des détails
debugPrint('Affichage des détails: ${passage['id']}');
_showPassageDetails(passage);
},
onPassageEdit: (passage) {
// Action lors de la modification d'un passage
debugPrint('Modification du passage: ${passage['id']}');
_editPassage(passage);
},
onReceiptView: (passage) {
// Action lors de la demande d'affichage du reçu
debugPrint(
'Affichage du reçu pour le passage: ${passage['id']}');
_showReceipt(passage);
},
),
],
),
],
),
),
],
),

View File

@@ -10,6 +10,7 @@ import 'package:geosector_app/presentation/widgets/mapbox_map.dart';
import '../../core/constants/app_keys.dart';
import '../../core/data/models/sector_model.dart';
import '../../core/data/models/passage_model.dart';
import '../../presentation/widgets/passage_map_dialog.dart';
// Extension pour ajouter ln2 (logarithme népérien de 2) comme constante
extension MathConstants on math.Random {
@@ -946,173 +947,17 @@ class _UserMapPageState extends State<UserMapPage> {
// Afficher les informations d'un passage lorsqu'on clique dessus
void _showPassageInfo(Map<String, dynamic> passage) {
final PassageModel passageModel = passage['model'] as PassageModel;
final int type = passageModel.fkType;
// Construire l'adresse complète
final String adresse =
'${passageModel.numero}, ${passageModel.rueBis} ${passageModel.rue}';
// Informations sur l'étage, l'appartement et la résidence (si habitat = 2)
String? etageInfo;
String? apptInfo;
String? residenceInfo;
if (passageModel.fkHabitat == 2) {
if (passageModel.niveau.isNotEmpty) {
etageInfo = 'Etage ${passageModel.niveau}';
}
if (passageModel.appt.isNotEmpty) {
apptInfo = 'appt. ${passageModel.appt}';
}
if (passageModel.residence.isNotEmpty) {
residenceInfo = passageModel.residence;
}
}
// Formater la date (uniquement si le type n'est pas 2 et si la date existe)
String dateInfo = '';
if (type != 2 && passageModel.passedAt != null) {
dateInfo = 'Date: ${_formatDate(passageModel.passedAt!)}';
}
// Récupérer le nom du passage (si le type n'est pas 6 - Maison vide)
String? nomInfo;
if (type != 6 && passageModel.name.isNotEmpty) {
nomInfo = passageModel.name;
}
// Récupérer les informations de règlement si le type est 1 (Effectué) ou 5 (Lot)
Widget? reglementInfo;
if (type == 1 || type == 5) {
final int typeReglementId = passageModel.fkTypeReglement;
final String montant = passageModel.montant;
// Récupérer les informations du type de règlement
if (AppKeys.typesReglements.containsKey(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;
reglementInfo = Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
Icon(iconData, color: couleur, size: 20),
const SizedBox(width: 8),
Text('$titre: $montant',
style:
TextStyle(color: couleur, fontWeight: FontWeight.bold)),
],
),
);
}
}
// Afficher une bulle d'information
showDialog(
context: context,
builder: (context) => AlertDialog(
contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Afficher en premier si le passage n'est pas affecté à un secteur
if (passageModel.fkSector == null) ...[
Container(
padding: const EdgeInsets.all(8),
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
border: Border.all(color: Colors.red, width: 1),
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
const Icon(Icons.warning, color: Colors.red, size: 20),
const SizedBox(width: 8),
const Expanded(
child: Text(
'Ce passage n\'est plus affecté à un secteur',
style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
),
),
],
),
),
],
Text('Adresse: $adresse'),
if (residenceInfo != null) ...[
const SizedBox(height: 4),
Text(residenceInfo)
],
if (etageInfo != null) ...[
const SizedBox(height: 4),
Text(etageInfo)
],
if (apptInfo != null) ...[
const SizedBox(height: 4),
Text(apptInfo)
],
if (dateInfo.isNotEmpty) ...[
const SizedBox(height: 8),
Text(dateInfo)
],
if (nomInfo != null) ...[
const SizedBox(height: 8),
Text('Nom: $nomInfo')
],
if (reglementInfo != null) reglementInfo,
],
),
actionsPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
// Bouton d'édition
IconButton(
onPressed: () {
Navigator.of(context).pop();
// Logique pour éditer le passage
debugPrint('Éditer le passage ${passageModel.id}');
},
icon: const Icon(Icons.edit),
color: Colors.blue,
tooltip: 'Modifier',
),
// Bouton de suppression
IconButton(
onPressed: () {
Navigator.of(context).pop();
// Logique pour supprimer le passage
debugPrint('Supprimer le passage ${passageModel.id}');
},
icon: const Icon(Icons.delete),
color: Colors.red,
tooltip: 'Supprimer',
),
],
),
// Bouton de fermeture
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
],
),
],
builder: (context) => PassageMapDialog(
passage: passageModel,
isAdmin: false, // L'utilisateur n'est pas admin
onDeleted: () {
// Recharger les passages après suppression
_loadPassages();
},
),
);
}
// Formater une date
String _formatDate(DateTime date) {
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
}
}

View File

@@ -60,6 +60,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
bool _chkStripe = false;
bool _chkMdpManuel = false;
bool _chkUsernameManuel = false;
bool _chkUserDeletePass = false;
// Pour l'upload du logo
final ImagePicker _picker = ImagePicker();
@@ -93,6 +94,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
_chkStripe = amicale?.chkStripe ?? false;
_chkMdpManuel = amicale?.chkMdpManuel ?? false;
_chkUsernameManuel = amicale?.chkUsernameManuel ?? false;
_chkUserDeletePass = amicale?.chkUserDeletePass ?? false;
// Note : Le logo sera chargé dynamiquement depuis l'API
}
@@ -152,6 +154,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
'chk_stripe': amicale.chkStripe ? 1 : 0,
'chk_mdp_manuel': amicale.chkMdpManuel ? 1 : 0,
'chk_username_manuel': amicale.chkUsernameManuel ? 1 : 0,
'chk_user_delete_pass': amicale.chkUserDeletePass ? 1 : 0,
};
// Ajouter les champs réservés aux administrateurs si l'utilisateur est admin
@@ -401,6 +404,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
chkStripe: _chkStripe,
chkMdpManuel: _chkMdpManuel,
chkUsernameManuel: _chkUsernameManuel,
chkUserDeletePass: _chkUserDeletePass,
) ??
AmicaleModel(
id: 0, // Sera remplacé par l'API
@@ -424,6 +428,7 @@ class _AmicaleFormState extends State<AmicaleForm> {
chkStripe: _chkStripe,
chkMdpManuel: _chkMdpManuel,
chkUsernameManuel: _chkUsernameManuel,
chkUserDeletePass: _chkUserDeletePass,
);
debugPrint('🔧 AmicaleModel créé: ${amicale.name}');
@@ -1159,6 +1164,20 @@ class _AmicaleFormState extends State<AmicaleForm> {
),
],
),
const SizedBox(height: 8),
// Checkbox pour autoriser les membres à supprimer des passages
_buildCheckboxOption(
label: "Autoriser les membres à supprimer des passages",
value: _chkUserDeletePass,
onChanged: widget.readOnly
? null
: (value) {
setState(() {
_chkUserDeletePass = value!;
});
},
),
const SizedBox(height: 25),
// Boutons Fermer et Enregistrer

View File

@@ -5,10 +5,8 @@ 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/presentation/widgets/passage_form_dialog.dart';
import 'package:geosector_app/core/utils/api_exception.dart';
import 'package:geosector_app/core/services/theme_service.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:go_router/go_router.dart';
/// AppBar personnalisée pour les tableaux de bord
@@ -19,12 +17,6 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
/// Le titre de la page actuelle (optionnel)
final String? pageTitle;
/// Indique si le bouton "Nouveau passage" doit être affiché
final bool showNewPassageButton;
/// Callback appelé lorsque le bouton "Nouveau passage" est pressé
final VoidCallback? onNewPassagePressed;
/// Indique si l'utilisateur est un administrateur
final bool isAdmin;
@@ -35,8 +27,6 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
super.key,
required this.title,
this.pageTitle,
this.showNewPassageButton = true,
this.onNewPassagePressed,
this.isAdmin = false,
this.onLogoutPressed,
});
@@ -166,42 +156,6 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
actions.add(const SizedBox(width: 8));
// Ajouter le bouton "Nouveau passage" seulement si l'utilisateur n'est pas admin
if (!isAdmin) {
actions.add(
TextButton.icon(
icon: const Icon(Icons.add_location_alt, color: Colors.white),
label: const Text('Nouveau passage',
style: TextStyle(color: Colors.white)),
onPressed: () {
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) => PassageFormDialog(
title: 'Nouveau passage',
passageRepository: passageRepository,
userRepository: userRepository,
operationRepository: operationRepository,
onSuccess: () {
// Callback après création du passage
if (onNewPassagePressed != null) {
onNewPassagePressed!();
}
},
),
);
},
style: TextButton.styleFrom(
backgroundColor: Color(AppKeys.typesPassages[1]!['couleur1']
as int), // Vert des passages effectués
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
),
);
actions.add(const SizedBox(width: 8));
}
// Ajouter le sélecteur de thème avec confirmation (désactivé temporairement)
// TODO: Réactiver quand le thème sombre sera corrigé
// actions.add(

View File

@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:geosector_app/presentation/widgets/dashboard_app_bar.dart';
import 'package:geosector_app/presentation/widgets/responsive_navigation.dart';
import 'package:geosector_app/app.dart'; // Pour accéder à userRepository
import 'package:geosector_app/core/theme/app_theme.dart'; // Pour les couleurs du thème
import 'dart:math' as math;
/// Layout commun pour les tableaux de bord utilisateur et administrateur
/// Combine DashboardAppBar et ResponsiveNavigation
@@ -23,12 +26,6 @@ class DashboardLayout extends StatelessWidget {
/// Actions supplémentaires à afficher dans l'AppBar
final List<Widget>? additionalActions;
/// Indique si le bouton "Nouveau passage" doit être affiché
final bool showNewPassageButton;
/// Callback appelé lorsque le bouton "Nouveau passage" est pressé
final VoidCallback? onNewPassagePressed;
/// Widgets à afficher en bas de la sidebar
final List<Widget>? sidebarBottomItems;
@@ -46,8 +43,6 @@ class DashboardLayout extends StatelessWidget {
required this.onDestinationSelected,
required this.destinations,
this.additionalActions,
this.showNewPassageButton = true,
this.onNewPassagePressed,
this.sidebarBottomItems,
this.isAdmin = false,
this.onLogoutPressed,
@@ -79,32 +74,57 @@ class DashboardLayout extends StatelessWidget {
);
}
return Scaffold(
backgroundColor: Colors
.transparent, // Fond transparent pour laisser voir le AdminBackground
appBar: DashboardAppBar(
title: title,
pageTitle: destinations[selectedIndex].label,
showNewPassageButton: showNewPassageButton,
onNewPassagePressed: onNewPassagePressed,
isAdmin: isAdmin,
onLogoutPressed: onLogoutPressed,
),
body: ResponsiveNavigation(
title:
title, // Même si le titre n'est pas affiché dans la navigation, il est utilisé pour la cohérence
body: body,
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
destinations: destinations,
// Ne pas afficher le bouton "Nouveau passage" dans la navigation car il est déjà dans l'AppBar
showNewPassageButton: false,
onNewPassagePressed: onNewPassagePressed,
sidebarBottomItems: sidebarBottomItems,
isAdmin: isAdmin,
// Ne pas afficher l'AppBar dans la navigation car nous utilisons DashboardAppBar
showAppBar: false,
),
// 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
return Stack(
children: [
// Fond dégradé avec points
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: gradientColors,
),
),
child: CustomPaint(
painter: DotsPainter(),
child: const SizedBox.expand(),
),
),
// Scaffold avec fond transparent
Scaffold(
backgroundColor: Colors.transparent,
appBar: DashboardAppBar(
title: title,
pageTitle: destinations[selectedIndex].label,
isAdmin: isAdmin,
onLogoutPressed: onLogoutPressed,
),
body: ResponsiveNavigation(
title:
title, // Même si le titre n'est pas affiché dans la navigation, il est utilisé pour la cohérence
body: body,
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
destinations: destinations,
// Ne pas afficher le bouton "Nouveau passage" dans la navigation
showNewPassageButton: false,
onNewPassagePressed: null,
sidebarBottomItems: sidebarBottomItems,
isAdmin: isAdmin,
// Ne pas afficher l'AppBar dans la navigation car nous utilisons DashboardAppBar
showAppBar: false,
),
),
],
);
} catch (e) {
debugPrint('ERREUR CRITIQUE dans DashboardLayout.build: $e');
@@ -141,3 +161,26 @@ class DashboardLayout extends StatelessWidget {
}
}
}
/// CustomPainter pour dessiner les petits points blancs sur le fond
class DotsPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white.withOpacity(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;
final radius = 1.0 + random.nextDouble() * 2.0;
canvas.drawCircle(Offset(x, y), radius, paint);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@@ -118,6 +118,7 @@ class _ResponsiveNavigationState extends State<ResponsiveNavigation> {
final isDesktop = size.width > 900;
return Scaffold(
backgroundColor: Colors.transparent, // Fond transparent pour voir le gradient
appBar: widget.showAppBar
? AppBar(
title: Text(widget.title),