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.
367 lines
11 KiB
Dart
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,
|
|
};
|
|
}
|
|
}
|
|
|