import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:hive_flutter/hive_flutter.dart'; import 'package:geosector_app/core/data/models/passage_model.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:geosector_app/core/utils/api_exception.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/app.dart'; /// Dialogue pour afficher les passages groupés d'un immeuble (fkHabitat=2) class GroupedPassagesDialog extends StatelessWidget { final PassageModel referencePassage; final bool isAdmin; const GroupedPassagesDialog({ super.key, required this.referencePassage, this.isAdmin = false, }); @override Widget build(BuildContext context) { // Construire l'adresse complète final String adresse = '${referencePassage.numero} ${referencePassage.rueBis} ${referencePassage.rue}' .trim(); final String ville = referencePassage.ville; final String residence = referencePassage.residence; // Calculer les dimensions final screenWidth = MediaQuery.of(context).size.width; final screenHeight = MediaQuery.of(context).size.height; final dialogWidth = kIsWeb ? 600.0 // Web : largeur fixe plus large : screenWidth * 0.9; // Mobile : 90% largeur final dialogHeight = screenHeight * 0.8; // 80% hauteur max // Vérifier si l'utilisateur peut supprimer bool canDelete = isAdmin; if (!isAdmin) { try { final amicale = CurrentAmicaleService.instance.currentAmicale; if (amicale != null) { canDelete = amicale.chkUserDeletePass == true; } } catch (e) { debugPrint('Erreur lors de la vérification des permissions: $e'); } } return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Container( width: dialogWidth, constraints: BoxConstraints( maxHeight: dialogHeight, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // En-tête avec adresse, ville, résidence et bouton X _buildHeader(context, adresse, ville, residence), const Divider(height: 1), // Liste des passages avec ValueListenableBuilder Flexible( child: ValueListenableBuilder>( valueListenable: Hive.box(AppKeys.passagesBoxName) .listenable(), builder: (context, box, child) { // Filtrer les passages de la même adresse final passages = _filterPassagesByAddress(box); if (passages.isEmpty) { return const Center( child: Padding( padding: EdgeInsets.all(24.0), child: Text('Aucun passage trouvé'), ), ); } return ListView.separated( shrinkWrap: true, itemCount: passages.length, separatorBuilder: (context, index) => const Divider(height: 1), itemBuilder: (context, index) { final passage = passages[index]; return _buildPassageItem(context, passage, canDelete); }, ); }, ), ), ], ), ), ); } /// Construire l'en-tête avec adresse, ville, résidence et boutons Widget _buildHeader( BuildContext context, String adresse, String ville, String residence) { return Container( padding: const EdgeInsets.all(16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Adresse if (adresse.isNotEmpty) Text( adresse, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), // Ville if (ville.isNotEmpty) ...[ const SizedBox(height: 4), Row( children: [ Icon(Icons.location_city, size: 16, color: Colors.grey[600]), const SizedBox(width: 4), Text( ville, style: TextStyle( fontSize: 14, color: Colors.grey[700], ), ), ], ), ], // Résidence if (residence.isNotEmpty) ...[ const SizedBox(height: 4), Row( children: [ Icon(Icons.apartment, size: 16, color: Colors.grey[600]), const SizedBox(width: 4), Text( residence, style: TextStyle( fontSize: 14, color: Colors.grey[700], ), ), ], ), ], ], ), ), // Bouton + pour ajouter un passage IconButton( onPressed: () => _showAddPassageDialog(context), icon: const Icon(Icons.add_circle, size: 28), tooltip: 'Ajouter un passage', color: Colors.green, padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), const SizedBox(width: 8), // Bouton X pour fermer IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close), tooltip: 'Fermer', padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), ); } /// Construire une ligne de passage Widget _buildPassageItem( BuildContext context, PassageModel passage, bool canDelete) { final int type = passage.fkType; // Récupérer la couleur2 du type final Color typeColor = Color(AppKeys.typesPassages[type]?['couleur2'] ?? 0xFF9E9E9E); // Niveau + Appt final String location = [ if (passage.niveau.isNotEmpty) 'Niv. ${passage.niveau}', if (passage.appt.isNotEmpty) 'Appt ${passage.appt}', ].join(', '); // Calculer le montant et vérifier s'il est payé final amount = _parseAmount(passage.montant); final isPaid = amount > 0; final formattedAmount = '${amount.toStringAsFixed(2).replaceAll('.', ',')} €'; return ListTile( dense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), onTap: () => _showEditDialog(context, passage), leading: Container( width: 12, height: 12, decoration: BoxDecoration( color: typeColor, shape: BoxShape.circle, ), ), title: Row( children: [ // Nom if (passage.name.isNotEmpty) Flexible( child: Text( passage.name, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), overflow: TextOverflow.ellipsis, ), ) else Text( 'Sans nom', style: TextStyle( fontSize: 14, color: Colors.grey[600], fontStyle: FontStyle.italic, ), ), ], ), subtitle: location.isNotEmpty || (isPaid && (type == 1 || type == 5)) ? _buildSubtitle(context, location, passage, isPaid, type, formattedAmount) : null, trailing: _buildTrailing(context, passage, canDelete), ); } /// Construire la ligne 2 (subtitle) avec Niveau/Appt + Badge montant Widget _buildSubtitle( BuildContext context, String location, PassageModel passage, bool isPaid, int type, String formattedAmount, ) { return Row( children: [ // Niveau + Appt if (location.isNotEmpty) Text( location, style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), const Spacer(), // Badge montant (si > 0 et type 1 ou 5) if (isPaid && (type == 1 || type == 5)) ...[ // Récupérer le type de règlement Builder( builder: (context) { final typeReglement = passage.fkTypeReglement; final reglementInfo = AppKeys.typesReglements[typeReglement]; final reglementIcon = reglementInfo?['icon_data'] as IconData? ?? Icons.help_outline; final reglementColor = Color(reglementInfo?['couleur'] as int? ?? 0xFF9E9E9E); return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), decoration: BoxDecoration( color: reglementColor.withOpacity(0.15), borderRadius: BorderRadius.circular(8), border: Border.all( color: reglementColor.withOpacity(0.4), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( reglementIcon, size: 12, color: reglementColor, ), const SizedBox(width: 4), Text( formattedAmount, style: TextStyle( color: reglementColor, fontWeight: FontWeight.bold, fontSize: 11, ), ), ], ), ); }, ), ], ], ); } /// Construire le trailing avec icône remarque et bouton delete (ligne 1) Widget? _buildTrailing( BuildContext context, PassageModel passage, bool canDelete, ) { final List trailingWidgets = []; // Icône remarque (si passage.remarque non vide) if (passage.remarque.isNotEmpty) { trailingWidgets.add( Icon( Icons.comment_outlined, size: 16, color: Colors.orange[700], ), ); } // Bouton delete if (canDelete) { if (trailingWidgets.isNotEmpty) { trailingWidgets.add(const SizedBox(width: 8)); } trailingWidgets.add( IconButton( onPressed: () => _showDeleteDialog(context, passage), icon: const Icon(Icons.delete, size: 20), tooltip: 'Supprimer', padding: const EdgeInsets.all(8), constraints: const BoxConstraints(), color: Colors.red, ), ); } // Retourner null si aucun widget, sinon Row if (trailingWidgets.isEmpty) return null; if (trailingWidgets.length == 1) return trailingWidgets.first; return Row( mainAxisSize: MainAxisSize.min, children: trailingWidgets, ); } /// Parser le montant depuis String vers double double _parseAmount(String montantStr) { if (montantStr.isEmpty) return 0.0; try { final cleaned = montantStr.replaceAll(',', '.'); return double.tryParse(cleaned) ?? 0.0; } catch (e) { return 0.0; } } /// Filtrer les passages par adresse et trier par niveau + appt List _filterPassagesByAddress(Box box) { // Clé d'adresse du passage de référence final referenceKey = '${referencePassage.numero}|${referencePassage.rueBis}|${referencePassage.rue}|${referencePassage.ville}'; // Filtrer les passages de la même adresse final passages = box.values.where((p) { final key = '${p.numero}|${p.rueBis}|${p.rue}|${p.ville}'; return key == referenceKey && p.fkHabitat == 2; }).toList(); // Trier par niveau puis appt passages.sort((a, b) { // Convertir niveau en int pour tri numérique final nivA = int.tryParse(a.niveau) ?? 0; final nivB = int.tryParse(b.niveau) ?? 0; if (nivA != nivB) { return nivA.compareTo(nivB); } // Si même niveau, trier par appt final apptA = a.appt.toLowerCase(); final apptB = b.appt.toLowerCase(); return apptA.compareTo(apptB); }); return passages; } /// Afficher le dialogue de modification void _showEditDialog(BuildContext context, PassageModel passage) { showDialog( context: context, builder: (BuildContext dialogContext) { return PassageFormDialog( passage: passage, title: 'Modifier le passage', passageRepository: passageRepository, userRepository: userRepository, operationRepository: operationRepository, amicaleRepository: amicaleRepository, // Pas de callback onSuccess - ValueListenableBuilder gère la réactivité ); }, ); } /// Afficher le dialogue d'ajout d'un passage pré-rempli void _showAddPassageDialog(BuildContext context) { // Créer un passage temporaire pré-rempli avec les infos de l'immeuble final newPassage = PassageModel( id: 0, // Nouveau passage fkOperation: referencePassage.fkOperation, fkSector: referencePassage.fkSector, fkUser: referencePassage.fkUser, fkType: 2, // Type "À finaliser" par défaut fkAdresse: referencePassage.fkAdresse, passedAt: DateTime.now(), numero: referencePassage.numero, rue: referencePassage.rue, rueBis: referencePassage.rueBis, ville: referencePassage.ville, residence: referencePassage.residence, fkHabitat: 2, // Appartement appt: '', // Vide pour saisie niveau: '', // Vide pour saisie gpsLat: referencePassage.gpsLat, gpsLng: referencePassage.gpsLng, nomRecu: '', remarque: '', montant: '0.00', fkTypeReglement: 4, emailErreur: '', nbPassages: 1, name: '', email: '', phone: '', stripePaymentId: null, lastSyncedAt: DateTime.now(), isActive: true, isSynced: false, ); showDialog( context: context, builder: (BuildContext dialogContext) { return PassageFormDialog( passage: newPassage, title: 'Nouveau passage dans l\'immeuble', passageRepository: passageRepository, userRepository: userRepository, operationRepository: operationRepository, amicaleRepository: amicaleRepository, // Pas de callback onSuccess - ValueListenableBuilder gère la réactivité ); }, ); } /// Afficher le dialogue de suppression void _showDeleteDialog(BuildContext context, PassageModel passage) { // Réutiliser le même système de confirmation que PassageMapDialog 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: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( fullAddress.isEmpty ? 'Adresse inconnue' : fullAddress, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 14, ), ), if (passage.niveau.isNotEmpty || passage.appt.isNotEmpty) ...[ const SizedBox(height: 4), Text( [ if (passage.niveau.isNotEmpty) 'Niveau ${passage.niveau}', if (passage.appt.isNotEmpty) 'Appt ${passage.appt}', ].join(', '), style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), ], if (passage.name.isNotEmpty) ...[ const SizedBox(height: 4), Text( passage.name, 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(context, passage); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), child: const Text('Supprimer définitivement'), ), ], ); }, ); } /// Supprimer un passage Future _deletePassage(BuildContext context, PassageModel passage) async { try { // Appeler le repository pour supprimer via l'API final success = await passageRepository.deletePassageViaApi(passage.id); if (success && context.mounted) { ApiException.showSuccess(context, 'Passage supprimé avec succès'); // Pas de callback - ValueListenableBuilder rafraîchit automatiquement } else if (context.mounted) { ApiException.showError( context, Exception('Erreur lors de la suppression')); } } catch (e) { debugPrint('Erreur suppression passage: $e'); if (context.mounted) { ApiException.showError(context, e); } } } }