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`.
401 lines
12 KiB
Dart
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;
|
|
}
|
|
}
|
|
}
|
|
|