Cette mise à jour majeure refactorise entièrement la gestion des utilisateurs pour la faire passer par des Cloud Functions sécurisées et migre une part importante de la logique métier (gestion des événements, maintenances, containers) du client vers le backend.
**Gestion des Utilisateurs (Backend & Frontend):**
- **Nouvelle fonction `createUserWithInvite` :**
- Crée l'utilisateur dans Firebase Auth avec un mot de passe temporaire.
- Crée le document utilisateur correspondant dans Firestore.
- Envoie automatiquement un e-mail de réinitialisation de mot de passe (via l'API REST de Firebase et `axios`) pour que l'utilisateur définisse son propre mot de passe, améliorant la sécurité et l'expérience d'intégration.
- **Refactorisation de `updateUser` et `deleteUser` :**
- Les anciennes fonctions `onCall` sont remplacées par des fonctions `onRequest` (HTTP) standards, alignées avec le reste de l'API.
- La logique de suppression gère désormais la suppression dans Auth et Firestore.
- **Réinitialisation de Mot de Passe (UI) :**
- Ajout d'un bouton "Réinitialiser le mot de passe" sur la carte utilisateur, permettant aux administrateurs d'envoyer un e-mail de réinitialisation à n'importe quel utilisateur.
- **Amélioration de l'UI :**
- Boîte de dialogue de confirmation améliorée pour la suppression d'un utilisateur.
- Notifications (Snackbars) pour les opérations de création, suppression et réinitialisation de mot de passe.
**Migration de la Logique Métier vers les Cloud Functions:**
- **Gestion de la Préparation d'Événements :**
- Migration complète de la logique de validation des étapes (préparation, chargement, déchargement, retour) du client vers de nouvelles Cloud Functions (`validateEquipmentPreparation`, `validateAllLoading`, etc.).
- Le backend gère désormais la mise à jour des statuts de l'événement (`inProgress`, `completed`) et des équipements (`inUse`, `available`).
- Le code frontend (`EventPreparationService`) a été simplifié pour appeler ces nouvelles fonctions au lieu d'effectuer des écritures directes sur Firestore.
- **Création de Maintenance :**
- La fonction `createMaintenance` gère maintenant la mise à jour des équipements associés (`maintenanceIds`) et la création d'alertes (`maintenanceDue`) si une maintenance est prévue prochainement. La logique client a été supprimée.
- **Suppression de Container :**
- La fonction `deleteContainer` a été améliorée pour nettoyer automatiquement les références (`parentBoxIds`) dans tous les équipements contenus avant de supprimer le container.
**Refactorisation et Corrections (Backend & Frontend) :**
- **Fiabilisation des Appels API (Frontend) :**
- Le `ApiService` a été renforcé pour convertir de manière plus robuste les données (notamment les `Map` de type `_JsonMap`) en JSON standard avant de les envoyer aux Cloud Functions, évitant ainsi des erreurs de sérialisation.
- **Correction des Références (Backend) :**
- La fonction `updateUser` convertit correctement les `roleId` (string) en `DocumentReference` Firestore.
- Sécurisation de la vérification de l'assignation d'un utilisateur à un événement (`workforce`) pour éviter les erreurs sur des références nulles.
- **Dépendance (Backend) :**
- Ajout de la librairie `axios` pour effectuer des appels à l'API REST de Firebase.
288 lines
8.5 KiB
Dart
288 lines
8.5 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:em2rp/models/container_model.dart';
|
|
import 'package:em2rp/models/equipment_model.dart';
|
|
import 'package:em2rp/services/container_service.dart';
|
|
import 'package:em2rp/services/data_service.dart';
|
|
import 'package:em2rp/services/api_service.dart';
|
|
|
|
class ContainerProvider with ChangeNotifier {
|
|
final ContainerService _containerService = ContainerService();
|
|
final DataService _dataService = DataService(FirebaseFunctionsApiService());
|
|
|
|
List<ContainerModel> _containers = [];
|
|
ContainerType? _selectedType;
|
|
EquipmentStatus? _selectedStatus;
|
|
String _searchQuery = '';
|
|
bool _isLoading = false;
|
|
bool _isInitialized = false;
|
|
|
|
List<ContainerModel> get containers => _containers;
|
|
ContainerType? get selectedType => _selectedType;
|
|
EquipmentStatus? get selectedStatus => _selectedStatus;
|
|
String get searchQuery => _searchQuery;
|
|
bool get isLoading => _isLoading;
|
|
bool get isInitialized => _isInitialized;
|
|
|
|
/// S'assure que les conteneurs sont chargés (charge si nécessaire)
|
|
Future<void> ensureLoaded() async {
|
|
if (_isInitialized || _isLoading) {
|
|
print('[ContainerProvider] Containers already loaded or loading, skipping...');
|
|
return;
|
|
}
|
|
print('[ContainerProvider] Containers not loaded, loading now...');
|
|
await loadContainers();
|
|
}
|
|
|
|
/// Charger tous les containers via l'API
|
|
Future<void> loadContainers() async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final containers = await _containerService.getContainers(
|
|
type: _selectedType,
|
|
status: _selectedStatus,
|
|
searchQuery: _searchQuery,
|
|
);
|
|
|
|
_containers = containers;
|
|
_isLoading = false;
|
|
_isInitialized = true;
|
|
notifyListeners();
|
|
} catch (e) {
|
|
print('Error loading containers: $e');
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// Récupérer les containers avec filtres appliqués
|
|
Future<List<ContainerModel>> getContainers() async {
|
|
return await _containerService.getContainers(
|
|
type: _selectedType,
|
|
status: _selectedStatus,
|
|
searchQuery: _searchQuery,
|
|
);
|
|
}
|
|
|
|
/// Stream des containers - retourne un stream depuis les données en cache
|
|
/// Pour compatibilité avec les widgets existants qui utilisent StreamBuilder
|
|
Stream<List<ContainerModel>> get containersStream async* {
|
|
// Si les données ne sont pas chargées, charger d'abord
|
|
if (!_isInitialized) {
|
|
await loadContainers();
|
|
}
|
|
|
|
// Émettre les données actuelles
|
|
yield _containers;
|
|
|
|
// Continuer à émettre les mises à jour du cache
|
|
// Note: Pour un vrai temps réel, il faudrait implémenter un StreamController
|
|
// et notifier quand les données changent
|
|
}
|
|
|
|
/// Définir le type sélectionné
|
|
/// Définir le type sélectionné
|
|
void setSelectedType(ContainerType? type) {
|
|
_selectedType = type;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Définir le statut sélectionné
|
|
void setSelectedStatus(EquipmentStatus? status) {
|
|
_selectedStatus = status;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Définir la requête de recherche
|
|
void setSearchQuery(String query) {
|
|
_searchQuery = query;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Créer un nouveau container
|
|
Future<void> createContainer(ContainerModel container) async {
|
|
await _containerService.createContainer(container);
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Mettre à jour un container
|
|
Future<void> updateContainer(String id, Map<String, dynamic> data) async {
|
|
await _containerService.updateContainer(id, data);
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Supprimer un container
|
|
Future<void> deleteContainer(String id) async {
|
|
await _containerService.deleteContainer(id);
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Récupérer un container par ID
|
|
Future<ContainerModel?> getContainerById(String id) async {
|
|
return await _containerService.getContainerById(id);
|
|
}
|
|
|
|
/// Charge plusieurs conteneurs par leurs IDs (optimisé pour les détails d'événement)
|
|
Future<List<ContainerModel>> getContainersByIds(List<String> containerIds) async {
|
|
if (containerIds.isEmpty) return [];
|
|
|
|
print('[ContainerProvider] Loading ${containerIds.length} containers by IDs...');
|
|
|
|
try {
|
|
// Vérifier d'abord le cache local
|
|
final cachedContainers = <ContainerModel>[];
|
|
final missingIds = <String>[];
|
|
|
|
for (final id in containerIds) {
|
|
final cached = _containers.firstWhere(
|
|
(c) => c.id == id,
|
|
orElse: () => ContainerModel(
|
|
id: '',
|
|
name: '',
|
|
type: ContainerType.flightCase,
|
|
status: EquipmentStatus.available,
|
|
equipmentIds: [],
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
),
|
|
);
|
|
|
|
if (cached.id.isNotEmpty) {
|
|
cachedContainers.add(cached);
|
|
} else {
|
|
missingIds.add(id);
|
|
}
|
|
}
|
|
|
|
print('[ContainerProvider] Found ${cachedContainers.length} in cache, ${missingIds.length} missing');
|
|
|
|
// Si tous sont en cache, retourner directement
|
|
if (missingIds.isEmpty) {
|
|
return cachedContainers;
|
|
}
|
|
|
|
// Charger les manquants depuis l'API
|
|
final containersData = await _dataService.getContainersByIds(missingIds);
|
|
|
|
final loadedContainers = containersData.map((data) {
|
|
return ContainerModel.fromMap(data, data['id'] as String);
|
|
}).toList();
|
|
|
|
// Ajouter au cache
|
|
for (final container in loadedContainers) {
|
|
if (!_containers.any((c) => c.id == container.id)) {
|
|
_containers.add(container);
|
|
}
|
|
}
|
|
|
|
print('[ContainerProvider] Loaded ${loadedContainers.length} containers from API');
|
|
|
|
// Retourner tous les conteneurs (cache + chargés)
|
|
return [...cachedContainers, ...loadedContainers];
|
|
} catch (e) {
|
|
print('[ContainerProvider] Error loading containers by IDs: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Ajouter un équipement à un container
|
|
Future<Map<String, dynamic>> addEquipmentToContainer({
|
|
required String containerId,
|
|
required String equipmentId,
|
|
String? userId,
|
|
}) async {
|
|
final result = await _containerService.addEquipmentToContainer(
|
|
containerId: containerId,
|
|
equipmentId: equipmentId,
|
|
userId: userId,
|
|
);
|
|
notifyListeners();
|
|
return result;
|
|
}
|
|
|
|
/// Retirer un équipement d'un container
|
|
Future<void> removeEquipmentFromContainer({
|
|
required String containerId,
|
|
required String equipmentId,
|
|
String? userId,
|
|
}) async {
|
|
await _containerService.removeEquipmentFromContainer(
|
|
containerId: containerId,
|
|
equipmentId: equipmentId,
|
|
userId: userId,
|
|
);
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Vérifier la disponibilité d'un container
|
|
Future<Map<String, dynamic>> checkContainerAvailability({
|
|
required String containerId,
|
|
required DateTime startDate,
|
|
required DateTime endDate,
|
|
String? excludeEventId,
|
|
}) async {
|
|
return await _containerService.checkContainerAvailability(
|
|
containerId: containerId,
|
|
startDate: startDate,
|
|
endDate: endDate,
|
|
excludeEventId: excludeEventId,
|
|
);
|
|
}
|
|
|
|
/// Récupérer les équipements d'un container
|
|
Future<List<EquipmentModel>> getContainerEquipment(String containerId) async {
|
|
return await _containerService.getContainerEquipment(containerId);
|
|
}
|
|
|
|
/// Trouver tous les containers contenant un équipement
|
|
Future<List<ContainerModel>> findContainersWithEquipment(String equipmentId) async {
|
|
return await _containerService.findContainersWithEquipment(equipmentId);
|
|
}
|
|
|
|
/// Vérifier si un ID existe
|
|
Future<bool> checkContainerIdExists(String id) async {
|
|
return await _containerService.checkContainerIdExists(id);
|
|
}
|
|
|
|
/// Générer un ID unique pour un container
|
|
/// Format: BOX_{TYPE}_{NAME}_{NUMBER}
|
|
static String generateContainerId({
|
|
required ContainerType type,
|
|
required String name,
|
|
int? number,
|
|
}) {
|
|
// Obtenir le type en majuscules
|
|
final typeStr = containerTypeToString(type);
|
|
|
|
// Nettoyer le nom (enlever espaces, caractères spéciaux)
|
|
final cleanName = name
|
|
.replaceAll(' ', '_')
|
|
.replaceAll(RegExp(r'[^a-zA-Z0-9_-]'), '')
|
|
.toUpperCase();
|
|
|
|
if (number != null) {
|
|
return 'BOX_${typeStr}_${cleanName}_#$number';
|
|
}
|
|
|
|
return 'BOX_${typeStr}_$cleanName';
|
|
}
|
|
|
|
/// Assurer l'unicité d'un ID de container
|
|
static Future<String> ensureUniqueContainerId(
|
|
String baseId,
|
|
ContainerService service,
|
|
) async {
|
|
String uniqueId = baseId;
|
|
int counter = 1;
|
|
|
|
while (await service.checkContainerIdExists(uniqueId)) {
|
|
uniqueId = '${baseId}_$counter';
|
|
counter++;
|
|
}
|
|
|
|
return uniqueId;
|
|
}
|
|
}
|
|
|