import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; enum EquipmentStatus { available, // Disponible inUse, // En prestation rented, // Loué lost, // Perdu outOfService, // HS maintenance, // En maintenance } String equipmentStatusToString(EquipmentStatus status) { switch (status) { case EquipmentStatus.available: return 'AVAILABLE'; case EquipmentStatus.inUse: return 'IN_USE'; case EquipmentStatus.rented: return 'RENTED'; case EquipmentStatus.lost: return 'LOST'; case EquipmentStatus.outOfService: return 'OUT_OF_SERVICE'; case EquipmentStatus.maintenance: return 'MAINTENANCE'; } } EquipmentStatus equipmentStatusFromString(String? status) { switch (status) { case 'AVAILABLE': return EquipmentStatus.available; case 'IN_USE': return EquipmentStatus.inUse; case 'RENTED': return EquipmentStatus.rented; case 'LOST': return EquipmentStatus.lost; case 'OUT_OF_SERVICE': return EquipmentStatus.outOfService; case 'MAINTENANCE': return EquipmentStatus.maintenance; default: return EquipmentStatus.available; } } enum EquipmentCategory { lighting, // Lumière sound, // Son video, // Vidéo effect, // Effets spéciaux structure, // Structure consumable, // Consommable cable, // Câble other // Autre } String equipmentCategoryToString(EquipmentCategory category) { switch (category) { case EquipmentCategory.lighting: return 'LIGHTING'; case EquipmentCategory.sound: return 'SOUND'; case EquipmentCategory.video: return 'VIDEO'; case EquipmentCategory.structure: return 'STRUCTURE'; case EquipmentCategory.consumable: return 'CONSUMABLE'; case EquipmentCategory.cable: return 'CABLE'; case EquipmentCategory.other: return 'OTHER'; case EquipmentCategory.effect: return 'EFFECT'; } } EquipmentCategory equipmentCategoryFromString(String? category) { switch (category) { case 'LIGHTING': return EquipmentCategory.lighting; case 'SOUND': return EquipmentCategory.sound; case 'VIDEO': return EquipmentCategory.video; case 'STRUCTURE': return EquipmentCategory.structure; case 'CONSUMABLE': return EquipmentCategory.consumable; case 'CABLE': return EquipmentCategory.cable; case 'EFFECT': return EquipmentCategory.effect; case 'OTHER': default: return EquipmentCategory.other; } } // Extensions pour centraliser les informations d'affichage extension EquipmentCategoryExtension on EquipmentCategory { /// Retourne le label français de la catégorie String get label { switch (this) { case EquipmentCategory.lighting: return 'Lumière'; case EquipmentCategory.sound: return 'Son'; case EquipmentCategory.video: return 'Vidéo'; case EquipmentCategory.effect: return 'Effets'; case EquipmentCategory.structure: return 'Structure'; case EquipmentCategory.consumable: return 'Consommable'; case EquipmentCategory.cable: return 'Câble'; case EquipmentCategory.other: return 'Autre'; } } /// Retourne l'icône Material de la catégorie IconData get iconData { switch (this) { case EquipmentCategory.lighting: return Icons.light_mode; case EquipmentCategory.sound: return Icons.volume_up; case EquipmentCategory.video: return Icons.videocam; case EquipmentCategory.effect: return Icons.auto_awesome; case EquipmentCategory.structure: return Icons.construction; case EquipmentCategory.consumable: return Icons.inventory_2; case EquipmentCategory.cable: return Icons.cable; case EquipmentCategory.other: return Icons.more_horiz; } } /// Retourne le chemin de l'icône personnalisée (si disponible) String? get customIconPath { switch (this) { case EquipmentCategory.structure: return 'assets/icons/truss.svg'; case EquipmentCategory.consumable: return 'assets/icons/tape.svg'; default: return null; } } /// Vérifie si une icône personnalisée est disponible bool get hasCustomIcon => customIconPath != null; /// Retourne l'icône Widget à afficher (unifié pour Material et personnalisé) Widget getIcon({double size = 24, Color? color}) { final customPath = customIconPath; if (customPath != null) { // Détection automatique du format (SVG ou PNG) final isSvg = customPath.toLowerCase().endsWith('.svg'); if (isSvg) { // SVG : on peut appliquer la couleur sans dégrader la qualité return SvgPicture.asset( customPath, width: size, height: size, colorFilter: color != null ? ColorFilter.mode(color, BlendMode.srcIn) : null, placeholderBuilder: (context) => Icon(iconData, size: size, color: color), ); } else { // PNG : on n'applique PAS le color filter pour préserver la qualité return Image.asset( customPath, width: size, height: size, filterQuality: FilterQuality.high, errorBuilder: (context, error, stackTrace) { return Icon(iconData, size: size, color: color); }, ); } } return Icon(iconData, size: size, color: color); } /// Version pour CircleAvatar et contextes similaires (sans ColorFilter si Material Icon) Widget getIconForAvatar({double size = 24, Color? color}) { final customPath = customIconPath; if (customPath != null) { final isSvg = customPath.toLowerCase().endsWith('.svg'); if (isSvg) { return SvgPicture.asset( customPath, width: size, height: size, colorFilter: color != null ? ColorFilter.mode(color, BlendMode.srcIn) : null, placeholderBuilder: (context) => Icon(iconData, size: size, color: color), ); } else { return Image.asset( customPath, width: size, height: size, filterQuality: FilterQuality.high, errorBuilder: (context, error, stackTrace) { return Icon(iconData, size: size, color: color); }, ); } } return Icon(iconData, size: size, color: color); } } extension EquipmentStatusExtension on EquipmentStatus { /// Retourne le label français du statut String get label { switch (this) { case EquipmentStatus.available: return 'Disponible'; case EquipmentStatus.inUse: return 'En prestation'; case EquipmentStatus.rented: return 'Loué'; case EquipmentStatus.lost: return 'Perdu'; case EquipmentStatus.outOfService: return 'HS'; case EquipmentStatus.maintenance: return 'Maintenance'; } } /// Retourne la couleur associée au statut Color get color { switch (this) { case EquipmentStatus.available: return Colors.green; case EquipmentStatus.inUse: return Colors.blue; case EquipmentStatus.rented: return Colors.orange; case EquipmentStatus.lost: return Colors.red; case EquipmentStatus.outOfService: return Colors.red.shade900; case EquipmentStatus.maintenance: return Colors.amber; } } } class EquipmentModel { final String id; // Identifiant unique (clé) final String name; // Nom de l'équipement final String? brand; // Marque (indexé) final String? model; // Modèle (indexé) final EquipmentCategory category; // Catégorie final EquipmentStatus status; // Statut actuel // Prix (visible uniquement avec manage_equipment) final double? purchasePrice; // Prix d'achat final double? rentalPrice; // Prix de location // Quantité (pour consommables/câbles) final int? totalQuantity; // Quantité totale final int? availableQuantity; // Quantité disponible final int? criticalThreshold; // Seuil critique pour alerte // Boîtes parentes (plusieurs possibles) final List parentBoxIds; // IDs des boîtes contenant cet équipement // Caractéristiques physiques final double? weight; // Poids (kg) final double? length; // Longueur (cm) final double? width; // Largeur (cm) final double? height; // Hauteur (cm) // Dates & maintenance final DateTime? purchaseDate; // Date d'achat final DateTime? lastMaintenanceDate; // Dernière maintenance final DateTime? nextMaintenanceDate; // Prochaine maintenance prévue // Maintenances (références) final List maintenanceIds; // IDs des opérations de maintenance // Image final String? imageUrl; // URL de l'image (Storage /materiel) // Métadonnées final String? notes; // Notes additionnelles final DateTime createdAt; // Date de création final DateTime updatedAt; // Date de mise à jour EquipmentModel({ required this.id, required this.name, this.brand, this.model, required this.category, this.status = EquipmentStatus.available, this.purchasePrice, this.rentalPrice, this.totalQuantity, this.availableQuantity, this.criticalThreshold, this.parentBoxIds = const [], this.weight, this.length, this.width, this.height, this.purchaseDate, this.lastMaintenanceDate, this.nextMaintenanceDate, this.maintenanceIds = const [], this.imageUrl, this.notes, required this.createdAt, required this.updatedAt, }); factory EquipmentModel.fromMap(Map map, String id) { // Gestion des listes final List parentBoxIdsRaw = map['parentBoxIds'] ?? []; final List parentBoxIds = parentBoxIdsRaw.map((e) => e.toString()).toList(); final List maintenanceIdsRaw = map['maintenanceIds'] ?? []; final List maintenanceIds = maintenanceIdsRaw.map((e) => e.toString()).toList(); return EquipmentModel( id: id, name: map['name'] ?? '', brand: map['brand'], model: map['model'], category: equipmentCategoryFromString(map['category']), status: equipmentStatusFromString(map['status']), purchasePrice: map['purchasePrice']?.toDouble(), rentalPrice: map['rentalPrice']?.toDouble(), totalQuantity: map['totalQuantity']?.toInt(), availableQuantity: map['availableQuantity']?.toInt(), criticalThreshold: map['criticalThreshold']?.toInt(), parentBoxIds: parentBoxIds, weight: map['weight']?.toDouble(), length: map['length']?.toDouble(), width: map['width']?.toDouble(), height: map['height']?.toDouble(), purchaseDate: (map['purchaseDate'] as Timestamp?)?.toDate(), nextMaintenanceDate: (map['nextMaintenanceDate'] as Timestamp?)?.toDate(), maintenanceIds: maintenanceIds, imageUrl: map['imageUrl'], notes: map['notes'], createdAt: (map['createdAt'] as Timestamp?)?.toDate() ?? DateTime.now(), updatedAt: (map['updatedAt'] as Timestamp?)?.toDate() ?? DateTime.now(), ); } Map toMap() { return { 'name': name, 'brand': brand, 'model': model, 'category': equipmentCategoryToString(category), 'status': equipmentStatusToString(status), 'purchasePrice': purchasePrice, 'rentalPrice': rentalPrice, 'totalQuantity': totalQuantity, 'availableQuantity': availableQuantity, 'criticalThreshold': criticalThreshold, 'parentBoxIds': parentBoxIds, 'weight': weight, 'length': length, 'width': width, 'height': height, 'lastMaintenanceDate': lastMaintenanceDate != null ? Timestamp.fromDate(lastMaintenanceDate!) : null, 'purchaseDate': purchaseDate != null ? Timestamp.fromDate(purchaseDate!) : null, 'nextMaintenanceDate': nextMaintenanceDate != null ? Timestamp.fromDate(nextMaintenanceDate!) : null, 'maintenanceIds': maintenanceIds, 'imageUrl': imageUrl, 'notes': notes, 'createdAt': Timestamp.fromDate(createdAt), 'updatedAt': Timestamp.fromDate(updatedAt), }; } EquipmentModel copyWith({ String? id, String? brand, String? name, String? model, EquipmentCategory? category, EquipmentStatus? status, double? purchasePrice, double? rentalPrice, int? totalQuantity, int? availableQuantity, int? criticalThreshold, List? parentBoxIds, double? weight, double? length, double? width, double? height, DateTime? purchaseDate, DateTime? lastMaintenanceDate, DateTime? nextMaintenanceDate, List? maintenanceIds, String? imageUrl, String? notes, DateTime? createdAt, DateTime? updatedAt, }) { return EquipmentModel( id: id ?? this.id, brand: brand ?? this.brand, name: name ?? this.name, model: model ?? this.model, category: category ?? this.category, status: status ?? this.status, purchasePrice: purchasePrice ?? this.purchasePrice, rentalPrice: rentalPrice ?? this.rentalPrice, totalQuantity: totalQuantity ?? this.totalQuantity, availableQuantity: availableQuantity ?? this.availableQuantity, criticalThreshold: criticalThreshold ?? this.criticalThreshold, parentBoxIds: parentBoxIds ?? this.parentBoxIds, weight: weight ?? this.weight, length: length ?? this.length, width: width ?? this.width, height: height ?? this.height, lastMaintenanceDate: lastMaintenanceDate ?? this.lastMaintenanceDate, purchaseDate: purchaseDate ?? this.purchaseDate, nextMaintenanceDate: nextMaintenanceDate ?? this.nextMaintenanceDate, maintenanceIds: maintenanceIds ?? this.maintenanceIds, imageUrl: imageUrl ?? this.imageUrl, notes: notes ?? this.notes, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, ); } // Helper pour vérifier si c'est un consommable/câble avec quantité bool get hasQuantity => category == EquipmentCategory.consumable || category == EquipmentCategory.cable; // Helper pour vérifier si le stock est critique bool get isCriticalStock { if (!hasQuantity || criticalThreshold == null || availableQuantity == null) { return false; } return availableQuantity! <= criticalThreshold!; } // Helper pour vérifier si la maintenance est à venir bool get isMaintenanceDue { if (nextMaintenanceDate == null) return false; return nextMaintenanceDate!.isBefore(DateTime.now().add(const Duration(days: 7))); } }