On release/v3.1.4: Sauvegarde temporaire pour changement de branche
This commit is contained in:
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user