Files
EM2_ERP/em2rp/lib/models/container_model.dart
ElPoyo df6d54a007 Refactor: Centralisation des labels et icônes pour les enums
Centralise la gestion des libellés, couleurs et icônes pour `EquipmentStatus`, `EquipmentCategory`, et `ContainerType` en utilisant des extensions Dart.

- Ajout de nouvelles icônes SVG pour `flight-case`, `truss` et `tape`.
- Refactorisation des vues pour utiliser les nouvelles extensions, supprimant ainsi la logique d'affichage dupliquée.
- Mise à jour des `ChoiceChip` et des listes de filtres pour afficher les icônes à côté des labels.
2025-10-29 18:43:24 +01:00

367 lines
11 KiB
Dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:em2rp/models/equipment_model.dart';
/// Type de container
enum ContainerType {
flightCase, // Flight case
pelicase, // Pelicase
bag, // Sac
openCrate, // Caisse ouverte
toolbox, // Boîte à outils
}
String containerTypeToString(ContainerType type) {
switch (type) {
case ContainerType.flightCase:
return 'FLIGHT_CASE';
case ContainerType.pelicase:
return 'PELICASE';
case ContainerType.bag:
return 'BAG';
case ContainerType.openCrate:
return 'OPEN_CRATE';
case ContainerType.toolbox:
return 'TOOLBOX';
}
}
ContainerType containerTypeFromString(String? type) {
switch (type) {
case 'FLIGHT_CASE':
return ContainerType.flightCase;
case 'PELICASE':
return ContainerType.pelicase;
case 'BAG':
return ContainerType.bag;
case 'OPEN_CRATE':
return ContainerType.openCrate;
case 'TOOLBOX':
return ContainerType.toolbox;
default:
return ContainerType.flightCase;
}
}
String containerTypeLabel(ContainerType type) {
switch (type) {
case ContainerType.flightCase:
return 'Flight Case';
case ContainerType.pelicase:
return 'Pelicase';
case ContainerType.bag:
return 'Sac';
case ContainerType.openCrate:
return 'Caisse Ouverte';
case ContainerType.toolbox:
return 'Boîte à Outils';
}
}
// Extensions pour centraliser les informations d'affichage
extension ContainerTypeExtension on ContainerType {
/// Retourne le label français du type de container
String get label {
switch (this) {
case ContainerType.flightCase:
return 'Flight Case';
case ContainerType.pelicase:
return 'Pelicase';
case ContainerType.bag:
return 'Sac';
case ContainerType.openCrate:
return 'Caisse Ouverte';
case ContainerType.toolbox:
return 'Boîte à Outils';
}
}
/// Retourne l'icône Material du type de container
IconData get iconData {
switch (this) {
case ContainerType.flightCase:
return Icons.work;
case ContainerType.pelicase:
return Icons.inventory_2;
case ContainerType.bag:
return Icons.shopping_bag;
case ContainerType.openCrate:
return Icons.inventory;
case ContainerType.toolbox:
return Icons.build_circle;
}
}
/// Retourne le chemin de l'icône personnalisée (si disponible)
String? get customIconPath {
switch (this) {
case ContainerType.flightCase:
return 'assets/icons/flight-case.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
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);
}
}
/// Modèle de container/boîte pour le matériel
class ContainerModel {
final String id; // Identifiant unique (généré comme pour équipement)
final String name; // Nom du container
final ContainerType type; // Type de container
final EquipmentStatus status; // Statut actuel (même que équipement)
// Caractéristiques physiques
final double? weight; // Poids à vide (kg)
final double? length; // Longueur (cm)
final double? width; // Largeur (cm)
final double? height; // Hauteur (cm)
// Contenu
final List<String> equipmentIds; // IDs des équipements contenus
// Événement
final String? eventId; // ID de l'événement actuel (si en prestation)
// Métadonnées
final String? notes; // Notes additionnelles
final DateTime createdAt; // Date de création
final DateTime updatedAt; // Date de mise à jour
// Historique simple (optionnel)
final List<ContainerHistoryEntry> history; // Historique des modifications
ContainerModel({
required this.id,
required this.name,
required this.type,
this.status = EquipmentStatus.available,
this.weight,
this.length,
this.width,
this.height,
this.equipmentIds = const [],
this.eventId,
this.notes,
required this.createdAt,
required this.updatedAt,
this.history = const [],
});
/// Vérifier si le container est vide
bool get isEmpty => equipmentIds.isEmpty;
/// Nombre d'équipements dans le container
int get itemCount => equipmentIds.length;
/// Calculer le volume (m³)
double? get volume {
if (length == null || width == null || height == null) return null;
return (length! * width! * height!) / 1000000; // cm³ to m³
}
/// Calculer le poids total (poids vide + équipements)
/// Nécessite la liste des équipements
double calculateTotalWeight(List<EquipmentModel> equipment) {
double total = weight ?? 0.0;
for (final eq in equipment) {
if (equipmentIds.contains(eq.id) && eq.weight != null) {
total += eq.weight!;
}
}
return total;
}
/// Factory depuis Firestore
factory ContainerModel.fromMap(Map<String, dynamic> map, String id) {
final List<dynamic> equipmentIdsRaw = map['equipmentIds'] ?? [];
final List<String> equipmentIds = equipmentIdsRaw.map((e) => e.toString()).toList();
final List<dynamic> historyRaw = map['history'] ?? [];
final List<ContainerHistoryEntry> history = historyRaw
.map((e) => ContainerHistoryEntry.fromMap(e as Map<String, dynamic>))
.toList();
return ContainerModel(
id: id,
name: map['name'] ?? '',
type: containerTypeFromString(map['type']),
status: equipmentStatusFromString(map['status']),
weight: map['weight']?.toDouble(),
length: map['length']?.toDouble(),
width: map['width']?.toDouble(),
height: map['height']?.toDouble(),
equipmentIds: equipmentIds,
eventId: map['eventId'],
notes: map['notes'],
createdAt: (map['createdAt'] as Timestamp?)?.toDate() ?? DateTime.now(),
updatedAt: (map['updatedAt'] as Timestamp?)?.toDate() ?? DateTime.now(),
history: history,
);
}
/// Convertir en Map pour Firestore
Map<String, dynamic> toMap() {
return {
'name': name,
'type': containerTypeToString(type),
'status': equipmentStatusToString(status),
'weight': weight,
'length': length,
'width': width,
'height': height,
'equipmentIds': equipmentIds,
'eventId': eventId,
'notes': notes,
'createdAt': Timestamp.fromDate(createdAt),
'updatedAt': Timestamp.fromDate(updatedAt),
'history': history.map((e) => e.toMap()).toList(),
};
}
/// Copier avec modifications
ContainerModel copyWith({
String? id,
String? name,
ContainerType? type,
EquipmentStatus? status,
double? weight,
double? length,
double? width,
double? height,
List<String>? equipmentIds,
String? eventId,
String? notes,
DateTime? createdAt,
DateTime? updatedAt,
List<ContainerHistoryEntry>? history,
}) {
return ContainerModel(
id: id ?? this.id,
name: name ?? this.name,
type: type ?? this.type,
status: status ?? this.status,
weight: weight ?? this.weight,
length: length ?? this.length,
width: width ?? this.width,
height: height ?? this.height,
equipmentIds: equipmentIds ?? this.equipmentIds,
eventId: eventId ?? this.eventId,
notes: notes ?? this.notes,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
history: history ?? this.history,
);
}
}
/// Entrée d'historique pour un container
class ContainerHistoryEntry {
final DateTime timestamp;
final String action; // 'added', 'removed', 'status_change', etc.
final String? equipmentId; // ID de l'équipement concerné (si applicable)
final String? previousValue; // Valeur précédente
final String? newValue; // Nouvelle valeur
final String? userId; // ID de l'utilisateur ayant fait la modification
ContainerHistoryEntry({
required this.timestamp,
required this.action,
this.equipmentId,
this.previousValue,
this.newValue,
this.userId,
});
factory ContainerHistoryEntry.fromMap(Map<String, dynamic> map) {
return ContainerHistoryEntry(
timestamp: (map['timestamp'] as Timestamp?)?.toDate() ?? DateTime.now(),
action: map['action'] ?? '',
equipmentId: map['equipmentId'],
previousValue: map['previousValue'],
newValue: map['newValue'],
userId: map['userId'],
);
}
Map<String, dynamic> toMap() {
return {
'timestamp': Timestamp.fromDate(timestamp),
'action': action,
'equipmentId': equipmentId,
'previousValue': previousValue,
'newValue': newValue,
'userId': userId,
};
}
}