Files
EM2_ERP/em2rp/lib/services/equipment_service.dart
ElPoyo b79791ff7a refactor: Ajout des sous-catégories et refonte de la gestion de l'appartenance
Cette mise à jour structurelle améliore la classification des équipements en introduisant la notion de sous-catégories et supprime la gestion directe de l'appartenance d'un équipement à une boîte (`parentBoxIds`). L'appartenance est désormais uniquement définie côté conteneur. Une nouvelle catégorie "Régie / Backline" est également ajoutée.

**Changements majeurs :**

-   **Suppression de `parentBoxIds` sur `EquipmentModel` :**
    -   Le champ `parentBoxIds` a été retiré du modèle de données `EquipmentModel` et de toutes les logiques associées (création, mise à jour, copie).
    -   La responsabilité de lier un équipement à un conteneur est désormais exclusivement gérée par le `ContainerModel` via sa liste `equipmentIds`.
    -   La logique de synchronisation complexe dans `EquipmentFormPage` qui mettait à jour les conteneurs lors de la modification d'un équipement a été entièrement supprimée, simplifiant considérablement le code.
    -   Le sélecteur de boîtes parentes (`ParentBoxesSelector`) a été retiré du formulaire d'équipement.

-   **Ajout des sous-catégories :**
    -   Un champ optionnel `subCategory` (String) a été ajouté au `EquipmentModel`.
    -   Le formulaire de création/modification d'équipement inclut désormais un nouveau champ "Sous-catégorie" avec autocomplétion.
    -   Ce champ est contextuel : il propose des suggestions basées sur les sous-catégories existantes pour la catégorie principale sélectionnée (ex: "Console", "Micro" pour la catégorie "Son").
    -   La sous-catégorie est maintenant affichée sur les fiches de détail des équipements et dans les listes de la page de gestion, améliorant la visibilité du classement.

**Nouvelle catégorie d'équipement :**

-   Une nouvelle catégorie `backline` ("Régie / Backline") a été ajoutée à `EquipmentCategory` avec une icône (`Icons.piano`) et une couleur associée.

**Refactorisation et nettoyage :**

-   Le `EquipmentProvider` et `EquipmentService` ont été mis à jour pour charger et filtrer les sous-catégories.
-   De nombreuses instanciations d'un `EquipmentModel` vide (`dummy`) à travers l'application ont été nettoyées pour retirer la référence à `parentBoxIds`.

-   **Version de l'application :**
    -   La version a été incrémentée à `1.0.4`.
2026-01-17 12:07:20 +01:00

401 lines
12 KiB
Dart

import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/models/maintenance_model.dart';
import 'package:em2rp/models/container_model.dart';
import 'package:em2rp/services/api_service.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/maintenance_service.dart';
class EquipmentService {
final ApiService _apiService = apiService;
final DataService _dataService = DataService(apiService);
// ============================================================================
// CRUD Operations - Utilise le backend sécurisé
// ============================================================================
/// Créer un nouvel équipement (via Cloud Function)
Future<void> createEquipment(EquipmentModel equipment) async {
try {
if (equipment.id.isEmpty) {
throw Exception('L\'ID de l\'équipement est requis pour la création');
}
final data = equipment.toMap();
data['id'] = equipment.id; // S'assurer que l'ID est inclus
await _apiService.call('createEquipment', data);
} catch (e) {
print('Error creating equipment: $e');
rethrow;
}
}
/// Mettre à jour un équipement (via Cloud Function)
Future<void> updateEquipment(String id, Map<String, dynamic> data) async {
try {
if (data.isEmpty) {
throw Exception('Aucune donnée à mettre à jour');
}
await _apiService.call('updateEquipment', {
'equipmentId': id,
'data': data,
});
} catch (e) {
print('Error updating equipment: $e');
rethrow;
}
}
/// Supprimer un équipement (via Cloud Function)
Future<void> deleteEquipment(String id) async {
try {
await _apiService.call('deleteEquipment', {'equipmentId': id});
} catch (e) {
print('Error deleting equipment: $e');
rethrow;
}
}
// ============================================================================
// READ Operations - Utilise Firestore streams (temps réel)
// ============================================================================
/// Récupérer un équipement par ID
Future<EquipmentModel?> getEquipmentById(String id) async {
try {
final equipmentsData = await _dataService.getEquipmentsByIds([id]);
if (equipmentsData.isEmpty) return null;
return EquipmentModel.fromMap(equipmentsData.first, id);
} catch (e) {
print('Error getting equipment: $e');
rethrow;
}
}
/// Récupérer les équipements avec filtres
Future<List<EquipmentModel>> getEquipment({
EquipmentCategory? category,
EquipmentStatus? status,
String? model,
String? searchQuery,
}) async {
try {
final equipmentsData = await _dataService.getEquipments();
var equipmentList = equipmentsData
.map((data) => EquipmentModel.fromMap(data, data['id'] as String))
.toList();
// Filtres côté client
if (category != null) {
equipmentList = equipmentList
.where((e) => e.category == category)
.toList();
}
if (status != null) {
equipmentList = equipmentList
.where((e) => e.status == status)
.toList();
}
if (model != null && model.isNotEmpty) {
equipmentList = equipmentList
.where((e) => e.model == model)
.toList();
}
if (searchQuery != null && searchQuery.isNotEmpty) {
final lowerSearch = searchQuery.toLowerCase();
equipmentList = equipmentList.where((equipment) {
return equipment.name.toLowerCase().contains(lowerSearch) ||
(equipment.model?.toLowerCase().contains(lowerSearch) ?? false) ||
equipment.id.toLowerCase().contains(lowerSearch);
}).toList();
}
return equipmentList;
} catch (e) {
print('Error getting equipment: $e');
rethrow;
}
}
// ============================================================================
// Availability & Stock Management - Logique métier côté client
// ============================================================================
/// Vérifier la disponibilité d'un équipement pour une période donnée
Future<List<Map<String, dynamic>>> checkAvailability(
String equipmentId,
DateTime startDate,
DateTime endDate,
) async {
try {
final response = await _apiService.call('checkEquipmentAvailability', {
'equipmentId': equipmentId,
'startDate': startDate.toIso8601String(),
'endDate': endDate.toIso8601String(),
});
final conflicts = (response['conflicts'] as List?)
?.map((c) => c as Map<String, dynamic>)
.toList() ?? [];
return conflicts;
} catch (e) {
print('Error checking availability: $e');
rethrow;
}
}
/// Trouver des alternatives (même modèle) disponibles
Future<List<EquipmentModel>> findAlternatives(
String model,
DateTime startDate,
DateTime endDate,
) async {
try {
final response = await _apiService.call('findAlternativeEquipment', {
'model': model,
'startDate': startDate.toIso8601String(),
'endDate': endDate.toIso8601String(),
});
final alternatives = (response['alternatives'] as List?)
?.map((a) => EquipmentModel.fromMap(a as Map<String, dynamic>, a['id'] as String))
.toList() ?? [];
return alternatives;
} catch (e) {
print('Error finding alternatives: $e');
rethrow;
}
}
/// Mettre à jour le stock d'un consommable/câble
Future<void> updateStock(String id, int quantityChange) async {
try {
final equipment = await getEquipmentById(id);
if (equipment == null) {
throw Exception('Equipment not found');
}
if (!equipment.hasQuantity) {
throw Exception('Equipment does not have quantity tracking');
}
final newAvailableQuantity = (equipment.availableQuantity ?? 0) + quantityChange;
await updateEquipment(id, {
'availableQuantity': newAvailableQuantity,
});
// Vérifier si le seuil critique est atteint
if (equipment.criticalThreshold != null &&
newAvailableQuantity <= equipment.criticalThreshold!) {
await _createLowStockAlert(equipment);
}
} catch (e) {
print('Error updating stock: $e');
rethrow;
}
}
/// Vérifier les stocks critiques et créer des alertes
Future<void> checkCriticalStock() async {
try {
final equipmentsData = await _dataService.getEquipments();
for (var data in equipmentsData) {
final equipment = EquipmentModel.fromMap(data, data['id'] as String);
// Filtrer uniquement les consommables et câbles
if ((equipment.category == EquipmentCategory.consumable ||
equipment.category == EquipmentCategory.cable) &&
equipment.isCriticalStock) {
await _createLowStockAlert(equipment);
}
}
} catch (e) {
print('Error checking critical stock: $e');
rethrow;
}
}
/// Créer une alerte de stock faible
Future<void> _createLowStockAlert(EquipmentModel equipment) async {
try {
// Note: Cette fonction pourrait utiliser une Cloud Function dédiée dans le futur
// Pour l'instant, on utilise l'API directement pour éviter de créer trop de fonctions
// Cette méthode est appelée rarement et en arrière-plan
await _apiService.call('createAlert', {
'type': 'LOW_STOCK',
'title': 'Stock critique',
'message': 'Stock critique pour ${equipment.name} (${equipment.model ?? ""}): ${equipment.availableQuantity}/${equipment.criticalThreshold}',
'severity': 'HIGH',
'equipmentId': equipment.id,
});
} catch (e) {
print('Error creating low stock alert: $e');
// Ne pas rethrow pour ne pas bloquer le processus
}
}
/// Générer les données du QR code (ID de l'équipement)
String generateQRCodeData(String equipmentId) {
// Pour l'instant, on retourne simplement l'ID
// On pourrait aussi générer une URL complète : https://app.em2events.fr/equipment/$equipmentId
return equipmentId;
}
/// Récupérer tous les modèles uniques (pour l'indexation/autocomplete)
Future<List<String>> getAllModels() async {
try {
final equipmentsData = await _dataService.getEquipments();
final models = <String>{};
for (var data in equipmentsData) {
final model = data['model'] as String?;
if (model != null && model.isNotEmpty) {
models.add(model);
}
}
return models.toList()..sort();
} catch (e) {
print('Error getting all models: $e');
rethrow;
}
}
/// Récupérer toutes les marques uniques (pour l'indexation/autocomplete)
Future<List<String>> getAllBrands() async {
try {
final equipmentsData = await _dataService.getEquipments();
final brands = <String>{};
for (var data in equipmentsData) {
final brand = data['brand'] as String?;
if (brand != null && brand.isNotEmpty) {
brands.add(brand);
}
}
return brands.toList()..sort();
} catch (e) {
print('Error getting all brands: $e');
rethrow;
}
}
/// Récupérer les modèles filtrés par marque
Future<List<String>> getModelsByBrand(String brand) async {
try {
final equipmentsData = await _dataService.getEquipments();
final models = <String>{};
for (var data in equipmentsData) {
if (data['brand'] == brand) {
final model = data['model'] as String?;
if (model != null && model.isNotEmpty) {
models.add(model);
}
}
}
return models.toList()..sort();
} catch (e) {
print('Error getting models by brand: $e');
rethrow;
}
}
/// Récupérer les sous-catégories filtrées par catégorie
Future<List<String>> getSubCategoriesByCategory(EquipmentCategory category) async {
try {
final equipmentsData = await _dataService.getEquipments();
final subCategories = <String>{};
final categoryString = equipmentCategoryToString(category);
for (var data in equipmentsData) {
if (data['category'] == categoryString) {
final subCategory = data['subCategory'] as String?;
if (subCategory != null && subCategory.isNotEmpty) {
subCategories.add(subCategory);
}
}
}
return subCategories.toList()..sort();
} catch (e) {
print('Error getting subcategories by category: $e');
rethrow;
}
}
/// Vérifier si un ID existe déjà
Future<bool> isIdUnique(String id) async {
try {
final equipment = await getEquipmentById(id);
return equipment == null;
} catch (e) {
print('Error checking ID uniqueness: $e');
rethrow;
}
}
/// Récupérer toutes les boîtes/containers disponibles
Future<List<ContainerModel>> getBoxes() async {
try {
final containersData = await _dataService.getContainers();
final boxes = <ContainerModel>[];
for (var data in containersData) {
final id = data['id'] as String;
final container = ContainerModel.fromMap(data, id);
boxes.add(container);
}
return boxes;
} catch (e) {
print('Error getting boxes: $e');
rethrow;
}
}
/// Récupérer plusieurs équipements par leurs IDs
Future<List<EquipmentModel>> getEquipmentsByIds(List<String> ids) async {
try {
if (ids.isEmpty) return [];
final equipmentsData = await _dataService.getEquipmentsByIds(ids);
return equipmentsData
.map((data) => EquipmentModel.fromMap(data, data['id'] as String))
.toList();
} catch (e) {
print('Error getting equipments by IDs: $e');
rethrow;
}
}
/// Récupérer les maintenances pour un équipement
/// Note: Cette méthode est maintenant déléguée au MaintenanceService
/// pour éviter la duplication de code
Future<List<MaintenanceModel>> getMaintenancesForEquipment(String equipmentId) async {
try {
// Déléguer au MaintenanceService qui utilise déjà les Cloud Functions
final maintenanceService = MaintenanceService();
return await maintenanceService.getMaintenancesByEquipment(equipmentId);
} catch (e) {
print('Error getting maintenances for equipment: $e');
rethrow;
}
}
}