- Préparation de la nouvelle branche pour les évolutions - Mise à jour de la version vers 3.2.4 - Intégration des modifications en cours 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
947 lines
35 KiB
Dart
Executable File
947 lines
35 KiB
Dart
Executable File
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
|
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/core/theme/app_theme.dart';
|
|
import 'package:geosector_app/core/data/models/passage_model.dart';
|
|
import 'package:geosector_app/core/data/models/sector_model.dart';
|
|
import 'package:geosector_app/core/data/models/membre_model.dart';
|
|
import 'package:geosector_app/core/data/models/user_model.dart';
|
|
import 'package:geosector_app/core/repositories/passage_repository.dart';
|
|
import 'package:geosector_app/core/repositories/sector_repository.dart';
|
|
import 'package:geosector_app/core/repositories/user_repository.dart';
|
|
import 'package:geosector_app/core/repositories/membre_repository.dart';
|
|
import 'package:geosector_app/presentation/widgets/passages/passages_list_widget.dart';
|
|
import 'package:geosector_app/presentation/widgets/passage_form_dialog.dart';
|
|
import 'dart:math' as math;
|
|
|
|
/// Class 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.withValues(alpha: 0.5)
|
|
..style = PaintingStyle.fill;
|
|
|
|
final random = math.Random(42); // Seed fixe pour consistance
|
|
final numberOfDots = (size.width * size.height) ~/ 1500;
|
|
|
|
for (int i = 0; i < numberOfDots; i++) {
|
|
final x = random.nextDouble() * size.width;
|
|
final y = random.nextDouble() * size.height;
|
|
final radius = 1.0 + random.nextDouble() * 2.0;
|
|
canvas.drawCircle(Offset(x, y), radius, paint);
|
|
}
|
|
}
|
|
|
|
@override
|
|
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});
|
|
|
|
@override
|
|
State<AdminHistoryPage> createState() => _AdminHistoryPageState();
|
|
}
|
|
|
|
class _AdminHistoryPageState extends State<AdminHistoryPage> {
|
|
// État du tri actuel
|
|
PassageSortType _currentSort = PassageSortType.dateDesc;
|
|
|
|
// Filtres présélectionnés depuis une autre page
|
|
int? selectedSectorId;
|
|
String selectedSector = 'Tous';
|
|
String selectedType = 'Tous';
|
|
|
|
// Listes pour les filtres
|
|
List<SectorModel> _sectors = [];
|
|
List<MembreModel> _membres = [];
|
|
|
|
// Repositories
|
|
late PassageRepository _passageRepository;
|
|
late SectorRepository _sectorRepository;
|
|
late UserRepository _userRepository;
|
|
late MembreRepository _membreRepository;
|
|
|
|
// Passages originaux pour l'édition
|
|
List<PassageModel> _originalPassages = [];
|
|
|
|
// État de chargement
|
|
bool _isLoading = true;
|
|
String _errorMessage = '';
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Initialiser les filtres
|
|
_initializeFilters();
|
|
// Charger les filtres présélectionnés depuis Hive si disponibles
|
|
_loadPreselectedFilters();
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
// Récupérer les repositories une seule fois
|
|
_loadRepositories();
|
|
}
|
|
|
|
// Charger les repositories et les données
|
|
void _loadRepositories() {
|
|
try {
|
|
// Utiliser les instances globales définies dans app.dart
|
|
_passageRepository = passageRepository;
|
|
_userRepository = userRepository;
|
|
_sectorRepository = sectorRepository;
|
|
_membreRepository = membreRepository;
|
|
|
|
// Charger les secteurs et les membres
|
|
_loadSectorsAndMembres();
|
|
|
|
// Charger les passages
|
|
_loadPassages();
|
|
} catch (e) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
_errorMessage = 'Erreur lors du chargement des repositories: $e';
|
|
});
|
|
}
|
|
}
|
|
|
|
// Charger les secteurs et les membres
|
|
void _loadSectorsAndMembres() {
|
|
try {
|
|
// Récupérer la liste des secteurs
|
|
_sectors = _sectorRepository.getAllSectors();
|
|
debugPrint('Nombre de secteurs récupérés: ${_sectors.length}');
|
|
|
|
// Récupérer la liste des membres
|
|
_membres = _membreRepository.getAllMembres();
|
|
debugPrint('Nombre de membres récupérés: ${_membres.length}');
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du chargement des secteurs et membres: $e');
|
|
}
|
|
}
|
|
|
|
// Charger les passages
|
|
void _loadPassages() {
|
|
setState(() {
|
|
_isLoading = true;
|
|
});
|
|
|
|
try {
|
|
// Récupérer les passages
|
|
final List<PassageModel> allPassages =
|
|
_passageRepository.getAllPassages();
|
|
|
|
// Stocker les passages originaux pour l'édition
|
|
_originalPassages = allPassages;
|
|
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
} catch (e) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
_errorMessage = 'Erreur lors du chargement des passages: $e';
|
|
});
|
|
}
|
|
}
|
|
|
|
// Initialiser les filtres
|
|
void _initializeFilters() {
|
|
// Par défaut, on n'applique pas de filtre présélectionné
|
|
selectedSectorId = null;
|
|
selectedSector = 'Tous';
|
|
selectedType = 'Tous';
|
|
}
|
|
|
|
// Charger les filtres présélectionnés depuis Hive
|
|
void _loadPreselectedFilters() {
|
|
try {
|
|
// Utiliser Hive directement sans async
|
|
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
|
|
final settingsBox = Hive.box(AppKeys.settingsBoxName);
|
|
|
|
// Charger le secteur présélectionné
|
|
final int? preselectedSectorId =
|
|
settingsBox.get('history_selectedSectorId');
|
|
final String? preselectedSectorName =
|
|
settingsBox.get('history_selectedSectorName');
|
|
final int? preselectedTypeId =
|
|
settingsBox.get('history_selectedTypeId');
|
|
|
|
if (preselectedSectorId != null && preselectedSectorName != null) {
|
|
selectedSectorId = preselectedSectorId;
|
|
selectedSector = preselectedSectorName;
|
|
|
|
debugPrint(
|
|
'Secteur présélectionné: $preselectedSectorName (ID: $preselectedSectorId)');
|
|
}
|
|
|
|
if (preselectedTypeId != null) {
|
|
selectedType = preselectedTypeId.toString();
|
|
debugPrint('Type de passage présélectionné: $preselectedTypeId');
|
|
}
|
|
|
|
// Nettoyer les valeurs après utilisation pour ne pas les réutiliser la prochaine fois
|
|
settingsBox.delete('history_selectedSectorId');
|
|
settingsBox.delete('history_selectedSectorName');
|
|
settingsBox.delete('history_selectedTypeId');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Erreur lors du chargement des filtres présélectionnés: $e');
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Afficher un widget de chargement ou d'erreur si nécessaire
|
|
if (_isLoading) {
|
|
return Stack(
|
|
children: [
|
|
// Fond dégradé avec petits points blancs
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [Colors.white, Colors.red.shade300],
|
|
),
|
|
),
|
|
child: CustomPaint(
|
|
painter: DotsPainter(),
|
|
child: const SizedBox(
|
|
width: double.infinity, height: double.infinity),
|
|
),
|
|
),
|
|
const Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
if (_errorMessage.isNotEmpty) {
|
|
return _buildErrorWidget(_errorMessage);
|
|
}
|
|
|
|
// Retourner le widget principal avec les données chargées
|
|
return Stack(
|
|
children: [
|
|
// Fond dégradé avec petits points blancs
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [Colors.white, Colors.red.shade300],
|
|
),
|
|
),
|
|
child: CustomPaint(
|
|
painter: DotsPainter(),
|
|
child:
|
|
const SizedBox(width: double.infinity, height: double.infinity),
|
|
),
|
|
),
|
|
// Contenu de la page
|
|
LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
// Padding responsive : réduit sur mobile pour maximiser l'espace
|
|
final screenWidth = MediaQuery.of(context).size.width;
|
|
final horizontalPadding = screenWidth < 600 ? 8.0 : 16.0;
|
|
final verticalPadding = 16.0;
|
|
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: horizontalPadding,
|
|
vertical: verticalPadding,
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
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();
|
|
|
|
// Convertir et formater les passages
|
|
final formattedPassages = _formatPassagesForWidget(
|
|
allPassages,
|
|
_sectorRepository,
|
|
_membreRepository);
|
|
|
|
// Récupérer les UserModel depuis les MembreModel
|
|
final users = _membres.map((membre) {
|
|
return userRepository.getUserById(membre.id);
|
|
}).where((user) => user != null).toList();
|
|
|
|
return PassagesListWidget(
|
|
// Données
|
|
passages: formattedPassages,
|
|
// Activation des filtres
|
|
showFilters: true,
|
|
showSearch: true,
|
|
showTypeFilter: true,
|
|
showPaymentFilter: true,
|
|
showSectorFilter: true,
|
|
showUserFilter: true,
|
|
showPeriodFilter: true,
|
|
// Données pour les filtres
|
|
sectors: _sectors,
|
|
members: users.cast<UserModel>(),
|
|
// Bouton d'ajout
|
|
showAddButton: true,
|
|
onAddPassage: () async {
|
|
// Ouvrir le dialogue de création de passage
|
|
await showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (BuildContext dialogContext) {
|
|
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
|
|
.withValues(alpha: 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
|
|
.withValues(alpha: 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,
|
|
),
|
|
],
|
|
),
|
|
// Actions
|
|
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);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// Widget d'erreur pour afficher un message d'erreur
|
|
Widget _buildErrorWidget(String message) {
|
|
return Stack(
|
|
children: [
|
|
// Fond dégradé avec petits points blancs
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [Colors.white, Colors.red.shade300],
|
|
),
|
|
),
|
|
child: CustomPaint(
|
|
painter: DotsPainter(),
|
|
child:
|
|
const SizedBox(width: double.infinity, height: double.infinity),
|
|
),
|
|
),
|
|
Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(
|
|
Icons.error_outline,
|
|
color: Colors.red,
|
|
size: 64,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Erreur',
|
|
style: TextStyle(
|
|
fontSize: AppTheme.r(context, 24),
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.red[700],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
message,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontSize: AppTheme.r(context, 16)),
|
|
),
|
|
const SizedBox(height: 24),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
// Recharger la page
|
|
setState(() {});
|
|
},
|
|
child: const Text('Réessayer'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// Convertir les passages du modèle Hive vers le format attendu par le widget
|
|
List<Map<String, dynamic>> _formatPassagesForWidget(
|
|
List<PassageModel> passages,
|
|
SectorRepository sectorRepository,
|
|
MembreRepository membreRepository) {
|
|
return passages.map((passage) {
|
|
// Récupérer le secteur associé au passage (si fkSector n'est pas null)
|
|
final SectorModel? sector = passage.fkSector != null
|
|
? sectorRepository.getSectorById(passage.fkSector!)
|
|
: null;
|
|
|
|
// Récupérer le membre associé au passage
|
|
final MembreModel? membre =
|
|
membreRepository.getMembreById(passage.fkUser);
|
|
|
|
// Construire l'adresse complète
|
|
final String address =
|
|
'${passage.numero} ${passage.rue}${passage.rueBis.isNotEmpty ? ' ${passage.rueBis}' : ''}, ${passage.ville}';
|
|
|
|
// 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!,
|
|
'address': address, // Adresse complète pour l'affichage
|
|
// Champs séparés pour l'édition
|
|
'numero': passage.numero,
|
|
'rueBis': passage.rueBis,
|
|
'rue': passage.rue,
|
|
'ville': passage.ville,
|
|
'residence': passage.residence,
|
|
'appt': passage.appt,
|
|
'niveau': passage.niveau,
|
|
'fkHabitat': passage.fkHabitat,
|
|
'fkSector': passage.fkSector,
|
|
'sector': sector?.libelle ?? 'Secteur inconnu',
|
|
'fkUser': passage.fkUser,
|
|
'user': membre?.name ?? 'Membre inconnu',
|
|
'type': passage.fkType,
|
|
'amount': double.tryParse(passage.montant) ?? 0.0,
|
|
'payment': passage.fkTypeReglement,
|
|
'email': passage.email,
|
|
'hasReceipt': passage.nomRecu.isNotEmpty,
|
|
'hasError': hasError,
|
|
'notes': passage.remarque,
|
|
'name': passage.name,
|
|
'phone': passage.phone,
|
|
'montant': passage.montant,
|
|
'remarque': passage.remarque,
|
|
// Autres champs utiles
|
|
'fkOperation': passage.fkOperation,
|
|
'passedAt': passage.passedAt,
|
|
'lastSyncedAt': passage.lastSyncedAt,
|
|
'isActive': passage.isActive,
|
|
'isSynced': passage.isSynced,
|
|
'isOwnedByCurrentUser':
|
|
passage.fkUser == currentUserId, // Ajout du champ pour le widget
|
|
};
|
|
}).toList();
|
|
}
|
|
|
|
void _showReceiptDialog(BuildContext context, Map<String, dynamic> passage) {
|
|
final int passageId = passage['id'] as int;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text('Reçu du passage #$passageId'),
|
|
content: const SizedBox(
|
|
width: 500,
|
|
height: 600,
|
|
child: Center(
|
|
child: Text('Aperçu du reçu PDF'),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('Fermer'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
// Action pour télécharger le reçu
|
|
Navigator.pop(context);
|
|
},
|
|
child: const Text('Télécharger'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// 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;
|
|
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: 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(context),
|
|
child: const Text('Fermer'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Méthode extraite pour ouvrir le dialog de modification
|
|
|
|
Widget _buildDetailRow(String label, String value) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SizedBox(
|
|
width: 150,
|
|
child: Text(
|
|
'$label :',
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Text(value),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHistoryItem(DateTime date, String user, String action) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'${date.day}/${date.month}/${date.year} à ${date.hour}h${date.minute.toString().padLeft(2, '0')}',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold, fontSize: AppTheme.r(context, 12)),
|
|
),
|
|
Text('$user - $action'),
|
|
const Divider(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// 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: [
|
|
Text(
|
|
'ATTENTION : Cette action est irréversible !',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.red,
|
|
fontSize: AppTheme.r(context, 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: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: AppTheme.r(context, 14),
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
if (passage['user'] != null)
|
|
Text(
|
|
'Collecteur: ${passage['user']}',
|
|
style: TextStyle(
|
|
fontSize: AppTheme.r(context, 12),
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
if (passage['date'] != null)
|
|
Text(
|
|
'Date: ${_formatDate(passage['date'] as DateTime)}',
|
|
style: TextStyle(
|
|
fontSize: AppTheme.r(context, 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}';
|
|
}
|
|
}
|