Add equipment management features (and qr generation support)

This commit is contained in:
ElPoyo
2025-10-21 16:32:18 +02:00
parent ef638d8c8c
commit ae3a1b7227
18 changed files with 4489 additions and 7 deletions

View File

@@ -0,0 +1,89 @@
import 'package:cloud_firestore/cloud_firestore.dart';
enum AlertType {
lowStock, // Stock faible
maintenanceDue, // Maintenance à venir
conflict // Conflit disponibilité
}
String alertTypeToString(AlertType type) {
switch (type) {
case AlertType.lowStock:
return 'LOW_STOCK';
case AlertType.maintenanceDue:
return 'MAINTENANCE_DUE';
case AlertType.conflict:
return 'CONFLICT';
}
}
AlertType alertTypeFromString(String? type) {
switch (type) {
case 'LOW_STOCK':
return AlertType.lowStock;
case 'MAINTENANCE_DUE':
return AlertType.maintenanceDue;
case 'CONFLICT':
return AlertType.conflict;
default:
return AlertType.conflict;
}
}
class AlertModel {
final String id; // ID généré automatiquement
final AlertType type; // Type d'alerte
final String message; // Message de l'alerte
final String? equipmentId; // ID de l'équipement concerné (optionnel)
final DateTime createdAt; // Date de création
final bool isRead; // Statut lu/non lu
AlertModel({
required this.id,
required this.type,
required this.message,
this.equipmentId,
required this.createdAt,
this.isRead = false,
});
factory AlertModel.fromMap(Map<String, dynamic> map, String id) {
return AlertModel(
id: id,
type: alertTypeFromString(map['type']),
message: map['message'] ?? '',
equipmentId: map['equipmentId'],
createdAt: (map['createdAt'] as Timestamp?)?.toDate() ?? DateTime.now(),
isRead: map['isRead'] ?? false,
);
}
Map<String, dynamic> toMap() {
return {
'type': alertTypeToString(type),
'message': message,
'equipmentId': equipmentId,
'createdAt': Timestamp.fromDate(createdAt),
'isRead': isRead,
};
}
AlertModel copyWith({
String? id,
AlertType? type,
String? message,
String? equipmentId,
DateTime? createdAt,
bool? isRead,
}) {
return AlertModel(
id: id ?? this.id,
type: type ?? this.type,
message: message ?? this.message,
equipmentId: equipmentId ?? this.equipmentId,
createdAt: createdAt ?? this.createdAt,
isRead: isRead ?? this.isRead,
);
}
}

View File

@@ -0,0 +1,279 @@
import 'package:cloud_firestore/cloud_firestore.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;
}
}
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<String> parentBoxIds; // IDs des boîtes contenant cet équipement
// 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<String> 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.purchaseDate,
this.lastMaintenanceDate,
this.nextMaintenanceDate,
this.maintenanceIds = const [],
this.imageUrl,
this.notes,
required this.createdAt,
required this.updatedAt,
});
factory EquipmentModel.fromMap(Map<String, dynamic> map, String id) {
// Gestion des listes
final List<dynamic> parentBoxIdsRaw = map['parentBoxIds'] ?? [];
final List<String> parentBoxIds = parentBoxIdsRaw.map((e) => e.toString()).toList();
final List<dynamic> maintenanceIdsRaw = map['maintenanceIds'] ?? [];
final List<String> 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,
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<String, dynamic> 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,
'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<String>? parentBoxIds,
DateTime? purchaseDate,
DateTime? lastMaintenanceDate,
DateTime? nextMaintenanceDate,
List<String>? 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,
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)));
}
}

View File

@@ -30,6 +30,128 @@ EventStatus eventStatusFromString(String? status) {
}
}
enum PreparationStatus {
notStarted,
inProgress,
completed,
completedWithMissing
}
String preparationStatusToString(PreparationStatus status) {
switch (status) {
case PreparationStatus.notStarted:
return 'NOT_STARTED';
case PreparationStatus.inProgress:
return 'IN_PROGRESS';
case PreparationStatus.completed:
return 'COMPLETED';
case PreparationStatus.completedWithMissing:
return 'COMPLETED_WITH_MISSING';
}
}
PreparationStatus preparationStatusFromString(String? status) {
switch (status) {
case 'NOT_STARTED':
return PreparationStatus.notStarted;
case 'IN_PROGRESS':
return PreparationStatus.inProgress;
case 'COMPLETED':
return PreparationStatus.completed;
case 'COMPLETED_WITH_MISSING':
return PreparationStatus.completedWithMissing;
default:
return PreparationStatus.notStarted;
}
}
enum ReturnStatus {
notStarted,
inProgress,
completed,
completedWithMissing
}
String returnStatusToString(ReturnStatus status) {
switch (status) {
case ReturnStatus.notStarted:
return 'NOT_STARTED';
case ReturnStatus.inProgress:
return 'IN_PROGRESS';
case ReturnStatus.completed:
return 'COMPLETED';
case ReturnStatus.completedWithMissing:
return 'COMPLETED_WITH_MISSING';
}
}
ReturnStatus returnStatusFromString(String? status) {
switch (status) {
case 'NOT_STARTED':
return ReturnStatus.notStarted;
case 'IN_PROGRESS':
return ReturnStatus.inProgress;
case 'COMPLETED':
return ReturnStatus.completed;
case 'COMPLETED_WITH_MISSING':
return ReturnStatus.completedWithMissing;
default:
return ReturnStatus.notStarted;
}
}
class EventEquipment {
final String equipmentId; // ID de l'équipement
final int quantity; // Quantité (pour consommables)
final bool isPrepared; // Validé en préparation
final bool isReturned; // Validé au retour
final int? returnedQuantity; // Quantité retournée (pour consommables)
EventEquipment({
required this.equipmentId,
this.quantity = 1,
this.isPrepared = false,
this.isReturned = false,
this.returnedQuantity,
});
factory EventEquipment.fromMap(Map<String, dynamic> map) {
return EventEquipment(
equipmentId: map['equipmentId'] ?? '',
quantity: map['quantity'] ?? 1,
isPrepared: map['isPrepared'] ?? false,
isReturned: map['isReturned'] ?? false,
returnedQuantity: map['returnedQuantity'],
);
}
Map<String, dynamic> toMap() {
return {
'equipmentId': equipmentId,
'quantity': quantity,
'isPrepared': isPrepared,
'isReturned': isReturned,
'returnedQuantity': returnedQuantity,
};
}
EventEquipment copyWith({
String? equipmentId,
int? quantity,
bool? isPrepared,
bool? isReturned,
int? returnedQuantity,
}) {
return EventEquipment(
equipmentId: equipmentId ?? this.equipmentId,
quantity: quantity ?? this.quantity,
isPrepared: isPrepared ?? this.isPrepared,
isReturned: isReturned ?? this.isReturned,
returnedQuantity: returnedQuantity ?? this.returnedQuantity,
);
}
}
class EventModel {
final String id;
final String name;
@@ -50,6 +172,11 @@ class EventModel {
final List<Map<String, dynamic>> options;
final EventStatus status;
// Nouveaux champs pour la gestion du matériel
final List<EventEquipment> assignedEquipment;
final PreparationStatus? preparationStatus;
final ReturnStatus? returnStatus;
EventModel({
required this.id,
required this.name,
@@ -69,6 +196,9 @@ class EventModel {
required this.documents,
this.options = const [],
this.status = EventStatus.waitingForApproval,
this.assignedEquipment = const [],
this.preparationStatus,
this.returnStatus,
});
factory EventModel.fromMap(Map<String, dynamic> map, String id) {
@@ -149,6 +279,22 @@ class EventModel {
customerId = map['customer'] as String;
}
// Gestion des équipements assignés
final assignedEquipmentRaw = map['assignedEquipment'] ?? [];
final List<EventEquipment> assignedEquipment = [];
if (assignedEquipmentRaw is List) {
for (var e in assignedEquipmentRaw) {
try {
if (e is Map) {
assignedEquipment.add(EventEquipment.fromMap(Map<String, dynamic>.from(e)));
}
} catch (equipmentError) {
print('Warning: Failed to parse equipment in event $id: $equipmentError');
}
}
}
return EventModel(
id: id,
name: (map['Name'] ?? '').toString().trim(),
@@ -168,6 +314,9 @@ class EventModel {
documents: docs,
options: options,
status: eventStatusFromString(map['status'] as String?),
assignedEquipment: assignedEquipment,
preparationStatus: preparationStatusFromString(map['preparationStatus'] as String?),
returnStatus: returnStatusFromString(map['returnStatus'] as String?),
);
} catch (e) {
print('Error parsing event $id: $e');
@@ -220,6 +369,9 @@ class EventModel {
'documents': documents,
'options': options,
'status': eventStatusToString(status),
'assignedEquipment': assignedEquipment.map((e) => e.toMap()).toList(),
'preparationStatus': preparationStatus != null ? preparationStatusToString(preparationStatus!) : null,
'returnStatus': returnStatus != null ? returnStatusToString(returnStatus!) : null,
};
}
}

View File

@@ -0,0 +1,138 @@
import 'package:cloud_firestore/cloud_firestore.dart';
enum MaintenanceType {
preventive, // Préventive
corrective, // Corrective
inspection // Inspection
}
String maintenanceTypeToString(MaintenanceType type) {
switch (type) {
case MaintenanceType.preventive:
return 'PREVENTIVE';
case MaintenanceType.corrective:
return 'CORRECTIVE';
case MaintenanceType.inspection:
return 'INSPECTION';
}
}
MaintenanceType maintenanceTypeFromString(String? type) {
switch (type) {
case 'PREVENTIVE':
return MaintenanceType.preventive;
case 'CORRECTIVE':
return MaintenanceType.corrective;
case 'INSPECTION':
return MaintenanceType.inspection;
default:
return MaintenanceType.preventive;
}
}
class MaintenanceModel {
final String id; // ID aléatoire
final List<String> equipmentIds; // IDs des équipements concernés (peut être multiple)
final MaintenanceType type; // Type de maintenance
final DateTime scheduledDate; // Date planifiée
final DateTime? completedDate; // Date de réalisation (null si pas encore effectuée)
final String name; // Nom de l'opération
final String description; // Description détaillée
final String? performedBy; // ID de l'utilisateur qui a effectué la maintenance
final double? cost; // Coût de la maintenance
final String? notes; // Notes additionnelles
final DateTime createdAt; // Date de création
final DateTime updatedAt; // Date de mise à jour
MaintenanceModel({
required this.id,
required this.equipmentIds,
required this.type,
required this.scheduledDate,
this.completedDate,
required this.name,
required this.description,
this.performedBy,
this.cost,
this.notes,
required this.createdAt,
required this.updatedAt,
});
factory MaintenanceModel.fromMap(Map<String, dynamic> map, String id) {
// Gestion de la liste des équipements
final List<dynamic> equipmentIdsRaw = map['equipmentIds'] ?? [];
final List<String> equipmentIds = equipmentIdsRaw.map((e) => e.toString()).toList();
return MaintenanceModel(
id: id,
equipmentIds: equipmentIds,
type: maintenanceTypeFromString(map['type']),
scheduledDate: (map['scheduledDate'] as Timestamp?)?.toDate() ?? DateTime.now(),
completedDate: (map['completedDate'] as Timestamp?)?.toDate(),
name: map['name'] ?? '',
description: map['description'] ?? '',
performedBy: map['performedBy'],
cost: map['cost']?.toDouble(),
notes: map['notes'],
createdAt: (map['createdAt'] as Timestamp?)?.toDate() ?? DateTime.now(),
updatedAt: (map['updatedAt'] as Timestamp?)?.toDate() ?? DateTime.now(),
);
}
Map<String, dynamic> toMap() {
return {
'equipmentIds': equipmentIds,
'type': maintenanceTypeToString(type),
'scheduledDate': Timestamp.fromDate(scheduledDate),
'completedDate': completedDate != null ? Timestamp.fromDate(completedDate!) : null,
'name': name,
'description': description,
'performedBy': performedBy,
'cost': cost,
'notes': notes,
'createdAt': Timestamp.fromDate(createdAt),
'updatedAt': Timestamp.fromDate(updatedAt),
};
}
MaintenanceModel copyWith({
String? id,
List<String>? equipmentIds,
MaintenanceType? type,
DateTime? scheduledDate,
DateTime? completedDate,
String? name,
String? description,
String? performedBy,
double? cost,
String? notes,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return MaintenanceModel(
id: id ?? this.id,
equipmentIds: equipmentIds ?? this.equipmentIds,
type: type ?? this.type,
scheduledDate: scheduledDate ?? this.scheduledDate,
completedDate: completedDate ?? this.completedDate,
name: name ?? this.name,
description: description ?? this.description,
performedBy: performedBy ?? this.performedBy,
cost: cost ?? this.cost,
notes: notes ?? this.notes,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
// Helper pour vérifier si la maintenance est complétée
bool get isCompleted => completedDate != null;
// Helper pour vérifier si la maintenance est en retard
bool get isOverdue {
if (isCompleted) return false;
return scheduledDate.isBefore(DateTime.now());
}
}