eac103491f
- **Recherche d'événements** : Ajout d'une fonctionnalité de recherche (titre, description, lieu) dans le calendrier et d'une nouvelle fonction Cloud `searchEvents` avec gestion des permissions.
- **Suppression d'équipement avec forçage** :
- Mise à jour de la fonction Cloud `deleteEquipment` pour détecter les assignations à des événements futurs.
- Ajout d'une option `forceDelete` pour passer outre les conflits d'assignation.
- Création de `EquipmentDeleteUtils` pour gérer uniformément les dialogues de confirmation et les erreurs de conflit (HTTP 409).
- Intégration de la logique de suppression sécurisée dans `EquipmentDetailPage` et `EquipmentManagementPage`.
- **Calendrier** :
- Refonte de l'interface mobile pour intégrer la barre de recherche.
- Optimisation du chargement des événements lors de la sélection d'un résultat de recherche (lazy loading du mois concerné).
- Amélioration de la stabilité de la sélection d'événements et du filtrage par utilisateur.
- **Services & Providers** :
- Amélioration de la gestion des erreurs dans `ApiService` pour faciliter le re-throw des exceptions personnalisées.
- Ajout du support de la suppression forcée dans `DataService` et `EquipmentProvider`.
- **Refactoring** : Nettoyage du code, amélioration du formatage et ajout de logs de debug dans les services de données et d'équipements.
132 lines
4.3 KiB
Dart
132 lines
4.3 KiB
Dart
import 'package:em2rp/services/api_service.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
/// Utilitaires partages pour la suppression d'equipement avec forcage.
|
|
class EquipmentDeleteUtils {
|
|
static const String _legacyConflictToken = 'future_event_assignment';
|
|
static const List<String> _conflictMessageTokens = [
|
|
'cannot delete equipment because it is assigned to upcoming events',
|
|
'cannot delete equipment because it is assigned to future events',
|
|
'assigned to upcoming events',
|
|
'assigned to future events',
|
|
];
|
|
|
|
static const String deleteDialogTitle = 'Confirmer la suppression';
|
|
static const String deleteDialogCancelLabel = 'Annuler';
|
|
static const String deleteDialogConfirmLabel = 'Supprimer';
|
|
static const String deleteSuccessMessage = 'Équipement supprimé avec succès';
|
|
|
|
/// Retourne [name] si renseigne, sinon [id].
|
|
static String resolveEquipmentLabel({required String id, String? name}) {
|
|
final trimmedName = name?.trim();
|
|
if (trimmedName == null || trimmedName.isEmpty) {
|
|
return id;
|
|
}
|
|
return trimmedName;
|
|
}
|
|
|
|
/// Construit le message de confirmation de suppression d'un equipement.
|
|
static String buildSingleDeleteConfirmationMessage(String equipmentLabel) {
|
|
return 'Voulez-vous vraiment supprimer "$equipmentLabel" ?\n\n'
|
|
'Cette action est irréversible.';
|
|
}
|
|
|
|
/// Construit le message de confirmation de suppression multiple.
|
|
static String buildBulkDeleteConfirmationMessage(int selectedCount) {
|
|
return 'Voulez-vous vraiment supprimer $selectedCount équipement(s) ?\n\n'
|
|
'Cette action est irréversible.';
|
|
}
|
|
|
|
/// Construit le message de succes de suppression multiple.
|
|
static String buildBulkDeleteSuccessMessage(int deletedCount) {
|
|
return '$deletedCount équipement(s) supprimé(s) avec succès';
|
|
}
|
|
|
|
/// Construit un message d'erreur de suppression homogene.
|
|
static String buildDeleteErrorMessage(Object error) {
|
|
return 'Erreur lors de la suppression : $error';
|
|
}
|
|
|
|
/// Indique si l'erreur correspond a un conflit de suppression 409.
|
|
static bool isFutureAssignmentDeleteConflict(Object error) {
|
|
if (error is ApiException && !error.isConflict) {
|
|
return false;
|
|
}
|
|
|
|
final normalizedMessage = _normalizeErrorMessage(error);
|
|
if (normalizedMessage.contains(_legacyConflictToken)) {
|
|
return true;
|
|
}
|
|
|
|
return _conflictMessageTokens.any(normalizedMessage.contains);
|
|
}
|
|
|
|
/// Affiche la confirmation de suppression forcee.
|
|
static Future<bool> showForceDeleteDialog(
|
|
BuildContext context, {
|
|
required String equipmentLabel,
|
|
}) async {
|
|
final shouldForceDelete = await showDialog<bool>(
|
|
context: context,
|
|
builder: (dialogContext) => AlertDialog(
|
|
title: const Text('Équipement utilisé dans un événement à venir'),
|
|
content: Text(
|
|
'"$equipmentLabel" est assigné à au moins un événement à venir.\n\n'
|
|
'Voulez-vous forcer la suppression ?',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(dialogContext, false),
|
|
child: const Text('Annuler'),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(dialogContext, true),
|
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
|
child: const Text('Forcer la suppression'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
return shouldForceDelete == true;
|
|
}
|
|
|
|
/// Execute une suppression, puis propose un forcage en cas de conflit 409.
|
|
static Future<bool> deleteWithFutureAssignmentCheck({
|
|
required BuildContext context,
|
|
required String equipmentLabel,
|
|
required Future<void> Function({bool forceDelete}) deleteEquipment,
|
|
}) async {
|
|
try {
|
|
await deleteEquipment(forceDelete: false);
|
|
return true;
|
|
} catch (error) {
|
|
if (!isFutureAssignmentDeleteConflict(error)) {
|
|
rethrow;
|
|
}
|
|
|
|
if (!context.mounted) {
|
|
return false;
|
|
}
|
|
|
|
final shouldForceDelete = await showForceDeleteDialog(
|
|
context,
|
|
equipmentLabel: equipmentLabel,
|
|
);
|
|
if (!shouldForceDelete) {
|
|
return false;
|
|
}
|
|
|
|
await deleteEquipment(forceDelete: true);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
static String _normalizeErrorMessage(Object error) {
|
|
if (error is ApiException) {
|
|
return error.message.toLowerCase();
|
|
}
|
|
return error.toString().toLowerCase();
|
|
}
|
|
}
|