Add equipment management features (and qr generation support)
This commit is contained in:
@@ -1,8 +1,12 @@
|
|||||||
import 'package:em2rp/providers/users_provider.dart';
|
import 'package:em2rp/providers/users_provider.dart';
|
||||||
import 'package:em2rp/providers/event_provider.dart';
|
import 'package:em2rp/providers/event_provider.dart';
|
||||||
|
import 'package:em2rp/providers/equipment_provider.dart';
|
||||||
|
import 'package:em2rp/providers/maintenance_provider.dart';
|
||||||
|
import 'package:em2rp/providers/alert_provider.dart';
|
||||||
import 'package:em2rp/utils/auth_guard_widget.dart';
|
import 'package:em2rp/utils/auth_guard_widget.dart';
|
||||||
import 'package:em2rp/views/calendar_page.dart';
|
import 'package:em2rp/views/calendar_page.dart';
|
||||||
import 'package:em2rp/views/login_page.dart';
|
import 'package:em2rp/views/login_page.dart';
|
||||||
|
import 'package:em2rp/views/equipment_management_page.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
@@ -43,6 +47,17 @@ void main() async {
|
|||||||
ChangeNotifierProvider<EventProvider>(
|
ChangeNotifierProvider<EventProvider>(
|
||||||
create: (context) => EventProvider(),
|
create: (context) => EventProvider(),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Providers pour la gestion du matériel
|
||||||
|
ChangeNotifierProvider<EquipmentProvider>(
|
||||||
|
create: (context) => EquipmentProvider(),
|
||||||
|
),
|
||||||
|
ChangeNotifierProvider<MaintenanceProvider>(
|
||||||
|
create: (context) => MaintenanceProvider(),
|
||||||
|
),
|
||||||
|
ChangeNotifierProvider<AlertProvider>(
|
||||||
|
create: (context) => AlertProvider(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: const MyApp(),
|
child: const MyApp(),
|
||||||
),
|
),
|
||||||
@@ -105,6 +120,9 @@ class MyApp extends StatelessWidget {
|
|||||||
actionCode: args['actionCode'] as String,
|
actionCode: args['actionCode'] as String,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
'/equipment_management': (context) => const AuthGuard(
|
||||||
|
requiredPermission: "view_equipment",
|
||||||
|
child: EquipmentManagementPage()),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
89
em2rp/lib/models/alert_model.dart
Normal file
89
em2rp/lib/models/alert_model.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
279
em2rp/lib/models/equipment_model.dart
Normal file
279
em2rp/lib/models/equipment_model.dart
Normal 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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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 {
|
class EventModel {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
@@ -50,6 +172,11 @@ class EventModel {
|
|||||||
final List<Map<String, dynamic>> options;
|
final List<Map<String, dynamic>> options;
|
||||||
final EventStatus status;
|
final EventStatus status;
|
||||||
|
|
||||||
|
// Nouveaux champs pour la gestion du matériel
|
||||||
|
final List<EventEquipment> assignedEquipment;
|
||||||
|
final PreparationStatus? preparationStatus;
|
||||||
|
final ReturnStatus? returnStatus;
|
||||||
|
|
||||||
EventModel({
|
EventModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
@@ -69,6 +196,9 @@ class EventModel {
|
|||||||
required this.documents,
|
required this.documents,
|
||||||
this.options = const [],
|
this.options = const [],
|
||||||
this.status = EventStatus.waitingForApproval,
|
this.status = EventStatus.waitingForApproval,
|
||||||
|
this.assignedEquipment = const [],
|
||||||
|
this.preparationStatus,
|
||||||
|
this.returnStatus,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory EventModel.fromMap(Map<String, dynamic> map, String id) {
|
factory EventModel.fromMap(Map<String, dynamic> map, String id) {
|
||||||
@@ -149,6 +279,22 @@ class EventModel {
|
|||||||
customerId = map['customer'] as String;
|
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(
|
return EventModel(
|
||||||
id: id,
|
id: id,
|
||||||
name: (map['Name'] ?? '').toString().trim(),
|
name: (map['Name'] ?? '').toString().trim(),
|
||||||
@@ -168,6 +314,9 @@ class EventModel {
|
|||||||
documents: docs,
|
documents: docs,
|
||||||
options: options,
|
options: options,
|
||||||
status: eventStatusFromString(map['status'] as String?),
|
status: eventStatusFromString(map['status'] as String?),
|
||||||
|
assignedEquipment: assignedEquipment,
|
||||||
|
preparationStatus: preparationStatusFromString(map['preparationStatus'] as String?),
|
||||||
|
returnStatus: returnStatusFromString(map['returnStatus'] as String?),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error parsing event $id: $e');
|
print('Error parsing event $id: $e');
|
||||||
@@ -220,6 +369,9 @@ class EventModel {
|
|||||||
'documents': documents,
|
'documents': documents,
|
||||||
'options': options,
|
'options': options,
|
||||||
'status': eventStatusToString(status),
|
'status': eventStatusToString(status),
|
||||||
|
'assignedEquipment': assignedEquipment.map((e) => e.toMap()).toList(),
|
||||||
|
'preparationStatus': preparationStatus != null ? preparationStatusToString(preparationStatus!) : null,
|
||||||
|
'returnStatus': returnStatus != null ? returnStatusToString(returnStatus!) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
138
em2rp/lib/models/maintenance_model.dart
Normal file
138
em2rp/lib/models/maintenance_model.dart
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
128
em2rp/lib/providers/alert_provider.dart
Normal file
128
em2rp/lib/providers/alert_provider.dart
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:em2rp/models/alert_model.dart';
|
||||||
|
|
||||||
|
class AlertProvider extends ChangeNotifier {
|
||||||
|
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||||
|
|
||||||
|
List<AlertModel> _alerts = [];
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
List<AlertModel> get alerts => _alerts;
|
||||||
|
|
||||||
|
/// Nombre d'alertes non lues
|
||||||
|
int get unreadCount => _alerts.where((alert) => !alert.isRead).length;
|
||||||
|
|
||||||
|
/// Alertes non lues uniquement
|
||||||
|
List<AlertModel> get unreadAlerts => _alerts.where((alert) => !alert.isRead).toList();
|
||||||
|
|
||||||
|
/// Alertes de stock critique
|
||||||
|
List<AlertModel> get lowStockAlerts => _alerts.where((alert) => alert.type == AlertType.lowStock).toList();
|
||||||
|
|
||||||
|
/// Alertes de maintenance
|
||||||
|
List<AlertModel> get maintenanceAlerts => _alerts.where((alert) => alert.type == AlertType.maintenanceDue).toList();
|
||||||
|
|
||||||
|
/// Alertes de conflit
|
||||||
|
List<AlertModel> get conflictAlerts => _alerts.where((alert) => alert.type == AlertType.conflict).toList();
|
||||||
|
|
||||||
|
/// Stream des alertes
|
||||||
|
Stream<List<AlertModel>> get alertsStream {
|
||||||
|
return _firestore
|
||||||
|
.collection('alerts')
|
||||||
|
.orderBy('createdAt', descending: true)
|
||||||
|
.snapshots()
|
||||||
|
.map((snapshot) {
|
||||||
|
_alerts = snapshot.docs
|
||||||
|
.map((doc) => AlertModel.fromMap(doc.data(), doc.id))
|
||||||
|
.toList();
|
||||||
|
return _alerts;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marquer une alerte comme lue
|
||||||
|
Future<void> markAsRead(String alertId) async {
|
||||||
|
try {
|
||||||
|
await _firestore.collection('alerts').doc(alertId).update({
|
||||||
|
'isRead': true,
|
||||||
|
});
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error marking alert as read: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marquer toutes les alertes comme lues
|
||||||
|
Future<void> markAllAsRead() async {
|
||||||
|
try {
|
||||||
|
final batch = _firestore.batch();
|
||||||
|
|
||||||
|
for (var alert in _alerts.where((a) => !a.isRead)) {
|
||||||
|
batch.update(
|
||||||
|
_firestore.collection('alerts').doc(alert.id),
|
||||||
|
{'isRead': true},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await batch.commit();
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error marking all alerts as read: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supprimer une alerte
|
||||||
|
Future<void> deleteAlert(String alertId) async {
|
||||||
|
try {
|
||||||
|
await _firestore.collection('alerts').doc(alertId).delete();
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error deleting alert: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supprimer toutes les alertes lues
|
||||||
|
Future<void> deleteReadAlerts() async {
|
||||||
|
try {
|
||||||
|
final batch = _firestore.batch();
|
||||||
|
|
||||||
|
for (var alert in _alerts.where((a) => a.isRead)) {
|
||||||
|
batch.delete(_firestore.collection('alerts').doc(alert.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
await batch.commit();
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error deleting read alerts: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Créer une alerte (utilisé principalement par les services)
|
||||||
|
Future<void> createAlert(AlertModel alert) async {
|
||||||
|
try {
|
||||||
|
await _firestore.collection('alerts').doc(alert.id).set(alert.toMap());
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error creating alert: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupérer les alertes pour un équipement spécifique
|
||||||
|
Stream<List<AlertModel>> getAlertsForEquipment(String equipmentId) {
|
||||||
|
return _firestore
|
||||||
|
.collection('alerts')
|
||||||
|
.where('equipmentId', isEqualTo: equipmentId)
|
||||||
|
.orderBy('createdAt', descending: true)
|
||||||
|
.snapshots()
|
||||||
|
.map((snapshot) {
|
||||||
|
return snapshot.docs
|
||||||
|
.map((doc) => AlertModel.fromMap(doc.data(), doc.id))
|
||||||
|
.toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
217
em2rp/lib/providers/equipment_provider.dart
Normal file
217
em2rp/lib/providers/equipment_provider.dart
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
|
import 'package:em2rp/services/equipment_service.dart';
|
||||||
|
|
||||||
|
class EquipmentProvider extends ChangeNotifier {
|
||||||
|
final EquipmentService _service = EquipmentService();
|
||||||
|
|
||||||
|
List<EquipmentModel> _equipment = [];
|
||||||
|
List<String> _models = [];
|
||||||
|
List<String> _brands = [];
|
||||||
|
|
||||||
|
EquipmentCategory? _selectedCategory;
|
||||||
|
EquipmentStatus? _selectedStatus;
|
||||||
|
String? _selectedModel;
|
||||||
|
String _searchQuery = '';
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
List<EquipmentModel> get equipment => _equipment;
|
||||||
|
List<String> get models => _models;
|
||||||
|
List<String> get brands => _brands;
|
||||||
|
EquipmentCategory? get selectedCategory => _selectedCategory;
|
||||||
|
EquipmentStatus? get selectedStatus => _selectedStatus;
|
||||||
|
String? get selectedModel => _selectedModel;
|
||||||
|
String get searchQuery => _searchQuery;
|
||||||
|
|
||||||
|
/// Stream des équipements avec filtres appliqués
|
||||||
|
Stream<List<EquipmentModel>> get equipmentStream {
|
||||||
|
return _service.getEquipment(
|
||||||
|
category: _selectedCategory,
|
||||||
|
status: _selectedStatus,
|
||||||
|
model: _selectedModel,
|
||||||
|
searchQuery: _searchQuery,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Charger tous les modèles uniques
|
||||||
|
Future<void> loadModels() async {
|
||||||
|
try {
|
||||||
|
_models = await _service.getAllModels();
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error loading models: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Charger toutes les marques uniques
|
||||||
|
Future<void> loadBrands() async {
|
||||||
|
try {
|
||||||
|
_brands = await _service.getAllBrands();
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error loading brands: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Charger les modèles filtrés par marque
|
||||||
|
Future<List<String>> loadModelsByBrand(String brand) async {
|
||||||
|
try {
|
||||||
|
return await _service.getModelsByBrand(brand);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error loading models by brand: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Ajouter un équipement
|
||||||
|
Future<void> addEquipment(EquipmentModel equipment) async {
|
||||||
|
try {
|
||||||
|
await _service.createEquipment(equipment);
|
||||||
|
|
||||||
|
// Recharger les modèles si un nouveau modèle a été ajouté
|
||||||
|
if (equipment.model != null && !_models.contains(equipment.model)) {
|
||||||
|
await loadModels();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error adding equipment: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mettre à jour un équipement
|
||||||
|
Future<void> updateEquipment(String id, Map<String, dynamic> data) async {
|
||||||
|
try {
|
||||||
|
await _service.updateEquipment(id, data);
|
||||||
|
|
||||||
|
// Recharger les modèles si le modèle a changé
|
||||||
|
if (data.containsKey('model')) {
|
||||||
|
await loadModels();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error updating equipment: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supprimer un équipement
|
||||||
|
Future<void> deleteEquipment(String id) async {
|
||||||
|
try {
|
||||||
|
await _service.deleteEquipment(id);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error deleting equipment: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupérer un équipement par ID
|
||||||
|
Future<EquipmentModel?> getEquipmentById(String id) async {
|
||||||
|
try {
|
||||||
|
return await _service.getEquipmentById(id);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error getting equipment: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trouver des alternatives disponibles
|
||||||
|
Future<List<EquipmentModel>> findAlternatives(
|
||||||
|
String model,
|
||||||
|
DateTime startDate,
|
||||||
|
DateTime endDate,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
return await _service.findAlternatives(model, startDate, endDate);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error finding alternatives: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifier la disponibilité d'un équipement
|
||||||
|
Future<List<String>> checkAvailability(
|
||||||
|
String equipmentId,
|
||||||
|
DateTime startDate,
|
||||||
|
DateTime endDate,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
return await _service.checkAvailability(equipmentId, startDate, endDate);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error checking availability: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mettre à jour le stock d'un consommable
|
||||||
|
Future<void> updateStock(String id, int quantityChange) async {
|
||||||
|
try {
|
||||||
|
await _service.updateStock(id, quantityChange);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error updating stock: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifier les stocks critiques
|
||||||
|
Future<void> checkCriticalStock() async {
|
||||||
|
try {
|
||||||
|
await _service.checkCriticalStock();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error checking critical stock: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Générer les données du QR code
|
||||||
|
String generateQRCodeData(String equipmentId) {
|
||||||
|
return _service.generateQRCodeData(equipmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifier si un ID est unique
|
||||||
|
Future<bool> isIdUnique(String id) async {
|
||||||
|
try {
|
||||||
|
return await _service.isIdUnique(id);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error checking ID uniqueness: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === FILTRES ===
|
||||||
|
|
||||||
|
/// Définir la catégorie sélectionnée
|
||||||
|
void setSelectedCategory(EquipmentCategory? category) {
|
||||||
|
_selectedCategory = category;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Définir le statut sélectionné
|
||||||
|
void setSelectedStatus(EquipmentStatus? status) {
|
||||||
|
_selectedStatus = status;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Définir le modèle sélectionné
|
||||||
|
void setSelectedModel(String? model) {
|
||||||
|
_selectedModel = model;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Définir la recherche
|
||||||
|
void setSearchQuery(String query) {
|
||||||
|
_searchQuery = query;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Réinitialiser tous les filtres
|
||||||
|
void resetFilters() {
|
||||||
|
_selectedCategory = null;
|
||||||
|
_selectedStatus = null;
|
||||||
|
_selectedModel = null;
|
||||||
|
_searchQuery = '';
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -30,9 +30,7 @@ class EventProvider with ChangeNotifier {
|
|||||||
for (var doc in eventsSnapshot.docs) {
|
for (var doc in eventsSnapshot.docs) {
|
||||||
try {
|
try {
|
||||||
final data = doc.data() as Map<String, dynamic>;
|
final data = doc.data() as Map<String, dynamic>;
|
||||||
print('Processing event ${doc.id}: ${data['Name'] ?? 'Unknown'}');
|
final event = EventModel.fromMap(data, doc.id);
|
||||||
|
|
||||||
final event = EventModel.fromMap(data, doc.id);
|
|
||||||
allEvents.add(event);
|
allEvents.add(event);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Failed to parse event ${doc.id}: $e');
|
print('Failed to parse event ${doc.id}: $e');
|
||||||
@@ -41,8 +39,6 @@ class EventProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print('Successfully parsed ${allEvents.length} events, failed: $failedCount');
|
|
||||||
|
|
||||||
// Filtrage amélioré pour les utilisateurs non-admin
|
// Filtrage amélioré pour les utilisateurs non-admin
|
||||||
if (canViewAllEvents) {
|
if (canViewAllEvents) {
|
||||||
_events = allEvents;
|
_events = allEvents;
|
||||||
@@ -63,8 +59,6 @@ class EventProvider with ChangeNotifier {
|
|||||||
|
|
||||||
return isInWorkforce;
|
return isInWorkforce;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
print('Non-admin user: showing ${_events.length} events out of ${allEvents.length}');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
|
|||||||
106
em2rp/lib/providers/maintenance_provider.dart
Normal file
106
em2rp/lib/providers/maintenance_provider.dart
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:em2rp/models/maintenance_model.dart';
|
||||||
|
import 'package:em2rp/services/maintenance_service.dart';
|
||||||
|
|
||||||
|
class MaintenanceProvider extends ChangeNotifier {
|
||||||
|
final MaintenanceService _service = MaintenanceService();
|
||||||
|
|
||||||
|
List<MaintenanceModel> _maintenances = [];
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
List<MaintenanceModel> get maintenances => _maintenances;
|
||||||
|
|
||||||
|
/// Stream des maintenances pour un équipement spécifique
|
||||||
|
Stream<List<MaintenanceModel>> getMaintenancesStream(String equipmentId) {
|
||||||
|
return _service.getMaintenances(equipmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stream de toutes les maintenances
|
||||||
|
Stream<List<MaintenanceModel>> get allMaintenancesStream {
|
||||||
|
return _service.getAllMaintenances();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Créer une nouvelle maintenance
|
||||||
|
Future<void> createMaintenance(MaintenanceModel maintenance) async {
|
||||||
|
try {
|
||||||
|
await _service.createMaintenance(maintenance);
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error creating maintenance: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mettre à jour une maintenance
|
||||||
|
Future<void> updateMaintenance(String id, Map<String, dynamic> data) async {
|
||||||
|
try {
|
||||||
|
await _service.updateMaintenance(id, data);
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error updating maintenance: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supprimer une maintenance
|
||||||
|
Future<void> deleteMaintenance(String id) async {
|
||||||
|
try {
|
||||||
|
await _service.deleteMaintenance(id);
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error deleting maintenance: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupérer une maintenance par ID
|
||||||
|
Future<MaintenanceModel?> getMaintenanceById(String id) async {
|
||||||
|
try {
|
||||||
|
return await _service.getMaintenanceById(id);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error getting maintenance: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marquer une maintenance comme complétée
|
||||||
|
Future<void> completeMaintenance(
|
||||||
|
String id, {
|
||||||
|
String? performedBy,
|
||||||
|
double? cost,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _service.completeMaintenance(id, performedBy: performedBy, cost: cost);
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error completing maintenance: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifier les maintenances à venir
|
||||||
|
Future<void> checkUpcomingMaintenances() async {
|
||||||
|
try {
|
||||||
|
await _service.checkUpcomingMaintenances();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error checking upcoming maintenances: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupérer les maintenances en retard
|
||||||
|
List<MaintenanceModel> get overdueMaintances {
|
||||||
|
return _maintenances.where((m) => m.isOverdue).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupérer les maintenances complétées
|
||||||
|
List<MaintenanceModel> get completedMaintenances {
|
||||||
|
return _maintenances.where((m) => m.isCompleted).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupérer les maintenances à venir
|
||||||
|
List<MaintenanceModel> get upcomingMaintenances {
|
||||||
|
return _maintenances.where((m) => !m.isCompleted && !m.isOverdue).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
373
em2rp/lib/services/equipment_service.dart
Normal file
373
em2rp/lib/services/equipment_service.dart
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
|
import 'package:em2rp/models/alert_model.dart';
|
||||||
|
|
||||||
|
class EquipmentService {
|
||||||
|
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||||
|
|
||||||
|
// Collection references
|
||||||
|
CollectionReference get _equipmentCollection => _firestore.collection('equipments');
|
||||||
|
CollectionReference get _alertsCollection => _firestore.collection('alerts');
|
||||||
|
CollectionReference get _eventsCollection => _firestore.collection('events');
|
||||||
|
|
||||||
|
// CRUD Operations
|
||||||
|
|
||||||
|
/// Créer un nouvel équipement
|
||||||
|
Future<void> createEquipment(EquipmentModel equipment) async {
|
||||||
|
try {
|
||||||
|
await _equipmentCollection.doc(equipment.id).set(equipment.toMap());
|
||||||
|
} catch (e) {
|
||||||
|
print('Error creating equipment: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mettre à jour un équipement
|
||||||
|
Future<void> updateEquipment(String id, Map<String, dynamic> data) async {
|
||||||
|
try {
|
||||||
|
data['updatedAt'] = Timestamp.fromDate(DateTime.now());
|
||||||
|
await _equipmentCollection.doc(id).update(data);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error updating equipment: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supprimer un équipement
|
||||||
|
Future<void> deleteEquipment(String id) async {
|
||||||
|
try {
|
||||||
|
await _equipmentCollection.doc(id).delete();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error deleting equipment: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupérer un équipement par ID
|
||||||
|
Future<EquipmentModel?> getEquipmentById(String id) async {
|
||||||
|
try {
|
||||||
|
final doc = await _equipmentCollection.doc(id).get();
|
||||||
|
if (doc.exists) {
|
||||||
|
return EquipmentModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error getting equipment: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupérer les équipements avec filtres
|
||||||
|
Stream<List<EquipmentModel>> getEquipment({
|
||||||
|
EquipmentCategory? category,
|
||||||
|
EquipmentStatus? status,
|
||||||
|
String? model,
|
||||||
|
String? searchQuery,
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
Query query = _equipmentCollection;
|
||||||
|
|
||||||
|
// Filtre par catégorie
|
||||||
|
if (category != null) {
|
||||||
|
query = query.where('category', isEqualTo: equipmentCategoryToString(category));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre par statut
|
||||||
|
if (status != null) {
|
||||||
|
query = query.where('status', isEqualTo: equipmentStatusToString(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtre par modèle
|
||||||
|
if (model != null && model.isNotEmpty) {
|
||||||
|
query = query.where('model', isEqualTo: model);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.snapshots().map((snapshot) {
|
||||||
|
List<EquipmentModel> equipmentList = snapshot.docs
|
||||||
|
.map((doc) => EquipmentModel.fromMap(doc.data() as Map<String, dynamic>, doc.id))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// Filtre par recherche texte (côté client car Firestore ne supporte pas les recherches texte complexes)
|
||||||
|
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 streaming equipment: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifier la disponibilité d'un équipement pour une période donnée
|
||||||
|
Future<List<String>> checkAvailability(
|
||||||
|
String equipmentId,
|
||||||
|
DateTime startDate,
|
||||||
|
DateTime endDate,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final conflicts = <String>[];
|
||||||
|
|
||||||
|
// Récupérer tous les événements qui chevauchent la période
|
||||||
|
final eventsQuery = await _eventsCollection
|
||||||
|
.where('StartDateTime', isLessThanOrEqualTo: Timestamp.fromDate(endDate))
|
||||||
|
.where('EndDateTime', isGreaterThanOrEqualTo: Timestamp.fromDate(startDate))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
for (var eventDoc in eventsQuery.docs) {
|
||||||
|
final eventData = eventDoc.data() as Map<String, dynamic>;
|
||||||
|
final assignedEquipmentRaw = eventData['assignedEquipment'] ?? [];
|
||||||
|
|
||||||
|
if (assignedEquipmentRaw is List) {
|
||||||
|
for (var eq in assignedEquipmentRaw) {
|
||||||
|
if (eq is Map && eq['equipmentId'] == equipmentId) {
|
||||||
|
conflicts.add(eventDoc.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// Récupérer tous les équipements du même modèle
|
||||||
|
final equipmentQuery = await _equipmentCollection
|
||||||
|
.where('model', isEqualTo: model)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
final alternatives = <EquipmentModel>[];
|
||||||
|
|
||||||
|
for (var doc in equipmentQuery.docs) {
|
||||||
|
final equipment = EquipmentModel.fromMap(
|
||||||
|
doc.data() as Map<String, dynamic>,
|
||||||
|
doc.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Vérifier la disponibilité
|
||||||
|
final conflicts = await checkAvailability(equipment.id, startDate, endDate);
|
||||||
|
|
||||||
|
if (conflicts.isEmpty && equipment.status == EquipmentStatus.available) {
|
||||||
|
alternatives.add(equipment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 equipmentQuery = await _equipmentCollection
|
||||||
|
.where('category', whereIn: [
|
||||||
|
equipmentCategoryToString(EquipmentCategory.consumable),
|
||||||
|
equipmentCategoryToString(EquipmentCategory.cable),
|
||||||
|
])
|
||||||
|
.get();
|
||||||
|
|
||||||
|
for (var doc in equipmentQuery.docs) {
|
||||||
|
final equipment = EquipmentModel.fromMap(
|
||||||
|
doc.data() as Map<String, dynamic>,
|
||||||
|
doc.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (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 {
|
||||||
|
// Vérifier si une alerte existe déjà pour cet équipement
|
||||||
|
final existingAlerts = await _alertsCollection
|
||||||
|
.where('equipmentId', isEqualTo: equipment.id)
|
||||||
|
.where('type', isEqualTo: alertTypeToString(AlertType.lowStock))
|
||||||
|
.where('isRead', isEqualTo: false)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (existingAlerts.docs.isEmpty) {
|
||||||
|
final alert = AlertModel(
|
||||||
|
id: _alertsCollection.doc().id,
|
||||||
|
type: AlertType.lowStock,
|
||||||
|
message: 'Stock critique pour ${equipment.name} (${equipment.model ?? ""}): ${equipment.availableQuantity}/${equipment.criticalThreshold}',
|
||||||
|
equipmentId: equipment.id,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _alertsCollection.doc(alert.id).set(alert.toMap());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error creating low stock alert: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 equipmentQuery = await _equipmentCollection.get();
|
||||||
|
final models = <String>{};
|
||||||
|
|
||||||
|
for (var doc in equipmentQuery.docs) {
|
||||||
|
final data = doc.data() as Map<String, dynamic>;
|
||||||
|
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 equipmentQuery = await _equipmentCollection.get();
|
||||||
|
final brands = <String>{};
|
||||||
|
|
||||||
|
for (var doc in equipmentQuery.docs) {
|
||||||
|
final data = doc.data() as Map<String, dynamic>;
|
||||||
|
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 equipmentQuery = await _equipmentCollection
|
||||||
|
.where('brand', isEqualTo: brand)
|
||||||
|
.get();
|
||||||
|
final models = <String>{};
|
||||||
|
|
||||||
|
for (var doc in equipmentQuery.docs) {
|
||||||
|
final data = doc.data() as Map<String, dynamic>;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifier si un ID existe déjà
|
||||||
|
Future<bool> isIdUnique(String id) async {
|
||||||
|
try {
|
||||||
|
final doc = await _equipmentCollection.doc(id).get();
|
||||||
|
return !doc.exists;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error checking ID uniqueness: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupérer toutes les boîtes (équipements qui peuvent contenir d'autres équipements)
|
||||||
|
Future<List<EquipmentModel>> getBoxes() async {
|
||||||
|
try {
|
||||||
|
// Les boîtes sont généralement des équipements de catégorie "structure" ou "other"
|
||||||
|
// On pourrait aussi ajouter un champ spécifique "isBox" dans le modèle
|
||||||
|
final equipmentQuery = await _equipmentCollection
|
||||||
|
.where('category', whereIn: [
|
||||||
|
equipmentCategoryToString(EquipmentCategory.structure),
|
||||||
|
equipmentCategoryToString(EquipmentCategory.other),
|
||||||
|
])
|
||||||
|
.get();
|
||||||
|
|
||||||
|
final boxes = <EquipmentModel>[];
|
||||||
|
for (var doc in equipmentQuery.docs) {
|
||||||
|
final equipment = EquipmentModel.fromMap(
|
||||||
|
doc.data() as Map<String, dynamic>,
|
||||||
|
doc.id,
|
||||||
|
);
|
||||||
|
// On pourrait ajouter un filtre supplémentaire ici si besoin
|
||||||
|
boxes.add(equipment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return boxes;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error getting boxes: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
363
em2rp/lib/services/event_preparation_service.dart
Normal file
363
em2rp/lib/services/event_preparation_service.dart
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:em2rp/models/event_model.dart';
|
||||||
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
|
import 'package:em2rp/services/equipment_service.dart';
|
||||||
|
|
||||||
|
class EventPreparationService {
|
||||||
|
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||||
|
final EquipmentService _equipmentService = EquipmentService();
|
||||||
|
|
||||||
|
// Collection references
|
||||||
|
CollectionReference get _eventsCollection => _firestore.collection('events');
|
||||||
|
CollectionReference get _equipmentCollection => _firestore.collection('equipment');
|
||||||
|
|
||||||
|
// === PRÉPARATION ===
|
||||||
|
|
||||||
|
/// Valider un équipement individuel en préparation
|
||||||
|
Future<void> validateEquipmentPreparation(String eventId, String equipmentId) async {
|
||||||
|
try {
|
||||||
|
final event = await _getEvent(eventId);
|
||||||
|
if (event == null) {
|
||||||
|
throw Exception('Event not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour le statut de l'équipement dans la liste
|
||||||
|
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||||
|
if (eq.equipmentId == equipmentId) {
|
||||||
|
return eq.copyWith(isPrepared: true);
|
||||||
|
}
|
||||||
|
return eq;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
await _eventsCollection.doc(eventId).update({
|
||||||
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print('Error validating equipment preparation: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Valider tous les équipements en préparation
|
||||||
|
Future<void> validateAllPreparation(String eventId) async {
|
||||||
|
try {
|
||||||
|
final event = await _getEvent(eventId);
|
||||||
|
if (event == null) {
|
||||||
|
throw Exception('Event not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marquer tous les équipements comme préparés
|
||||||
|
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||||
|
return eq.copyWith(isPrepared: true);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
await _eventsCollection.doc(eventId).update({
|
||||||
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
|
'preparationStatus': preparationStatusToString(PreparationStatus.completed),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mettre à jour le statut des équipements à "inUse"
|
||||||
|
for (var equipment in event.assignedEquipment) {
|
||||||
|
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.inUse);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error validating all preparation: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finaliser la préparation avec des équipements manquants
|
||||||
|
Future<void> completePreparationWithMissing(
|
||||||
|
String eventId,
|
||||||
|
List<String> missingEquipmentIds,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final event = await _getEvent(eventId);
|
||||||
|
if (event == null) {
|
||||||
|
throw Exception('Event not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marquer comme complété avec manquants
|
||||||
|
await _eventsCollection.doc(eventId).update({
|
||||||
|
'preparationStatus': preparationStatusToString(PreparationStatus.completedWithMissing),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mettre à jour le statut des équipements préparés à "inUse"
|
||||||
|
for (var equipment in event.assignedEquipment) {
|
||||||
|
if (equipment.isPrepared && !missingEquipmentIds.contains(equipment.equipmentId)) {
|
||||||
|
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.inUse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error completing preparation with missing: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === RETOUR ===
|
||||||
|
|
||||||
|
|
||||||
|
/// Valider le retour d'un équipement individuel
|
||||||
|
Future<void> validateEquipmentReturn(
|
||||||
|
String eventId,
|
||||||
|
String equipmentId, {
|
||||||
|
int? returnedQuantity,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final event = await _getEvent(eventId);
|
||||||
|
if (event == null) {
|
||||||
|
throw Exception('Event not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour le statut de l'équipement dans la liste
|
||||||
|
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||||
|
if (eq.equipmentId == equipmentId) {
|
||||||
|
return eq.copyWith(
|
||||||
|
isReturned: true,
|
||||||
|
returnedQuantity: returnedQuantity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return eq;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
await _eventsCollection.doc(eventId).update({
|
||||||
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mettre à jour le stock si c'est un consommable
|
||||||
|
if (returnedQuantity != null) {
|
||||||
|
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
|
||||||
|
if (equipmentDoc.exists) {
|
||||||
|
final equipment = EquipmentModel.fromMap(
|
||||||
|
equipmentDoc.data() as Map<String, dynamic>,
|
||||||
|
equipmentDoc.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (equipment.hasQuantity) {
|
||||||
|
final currentAvailable = equipment.availableQuantity ?? 0;
|
||||||
|
await _equipmentCollection.doc(equipmentId).update({
|
||||||
|
'availableQuantity': currentAvailable + returnedQuantity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error validating equipment return: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Valider tous les retours
|
||||||
|
Future<void> validateAllReturn(String eventId) async {
|
||||||
|
try {
|
||||||
|
final event = await _getEvent(eventId);
|
||||||
|
if (event == null) {
|
||||||
|
throw Exception('Event not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marquer tous les équipements comme retournés
|
||||||
|
final updatedEquipment = event.assignedEquipment.map((eq) {
|
||||||
|
return eq.copyWith(
|
||||||
|
isReturned: true,
|
||||||
|
returnedQuantity: eq.returnedQuantity ?? eq.quantity,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
await _eventsCollection.doc(eventId).update({
|
||||||
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
|
'returnStatus': returnStatusToString(ReturnStatus.completed),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mettre à jour le statut des équipements à "available" et gérer les stocks
|
||||||
|
for (var equipment in updatedEquipment) {
|
||||||
|
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.available);
|
||||||
|
|
||||||
|
// Restaurer le stock pour les consommables
|
||||||
|
final equipmentDoc = await _equipmentCollection.doc(equipment.equipmentId).get();
|
||||||
|
if (equipmentDoc.exists) {
|
||||||
|
final equipmentData = EquipmentModel.fromMap(
|
||||||
|
equipmentDoc.data() as Map<String, dynamic>,
|
||||||
|
equipmentDoc.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (equipmentData.hasQuantity && equipment.returnedQuantity != null) {
|
||||||
|
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
||||||
|
await _equipmentCollection.doc(equipment.equipmentId).update({
|
||||||
|
'availableQuantity': currentAvailable + equipment.returnedQuantity!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error validating all return: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finaliser le retour avec des équipements manquants
|
||||||
|
Future<void> completeReturnWithMissing(
|
||||||
|
String eventId,
|
||||||
|
List<String> missingEquipmentIds,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final event = await _getEvent(eventId);
|
||||||
|
if (event == null) {
|
||||||
|
throw Exception('Event not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marquer comme complété avec manquants
|
||||||
|
await _eventsCollection.doc(eventId).update({
|
||||||
|
'returnStatus': returnStatusToString(ReturnStatus.completedWithMissing),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mettre à jour le statut des équipements retournés à "available"
|
||||||
|
for (var equipment in event.assignedEquipment) {
|
||||||
|
if (equipment.isReturned && !missingEquipmentIds.contains(equipment.equipmentId)) {
|
||||||
|
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.available);
|
||||||
|
|
||||||
|
// Restaurer le stock pour les consommables
|
||||||
|
final equipmentDoc = await _equipmentCollection.doc(equipment.equipmentId).get();
|
||||||
|
if (equipmentDoc.exists) {
|
||||||
|
final equipmentData = EquipmentModel.fromMap(
|
||||||
|
equipmentDoc.data() as Map<String, dynamic>,
|
||||||
|
equipmentDoc.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (equipmentData.hasQuantity && equipment.returnedQuantity != null) {
|
||||||
|
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
||||||
|
await _equipmentCollection.doc(equipment.equipmentId).update({
|
||||||
|
'availableQuantity': currentAvailable + equipment.returnedQuantity!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (missingEquipmentIds.contains(equipment.equipmentId)) {
|
||||||
|
// Marquer comme perdu
|
||||||
|
await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.lost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error completing return with missing: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === HELPERS ===
|
||||||
|
|
||||||
|
/// Mettre à jour le statut d'un équipement
|
||||||
|
Future<void> updateEquipmentStatus(String equipmentId, EquipmentStatus status) async {
|
||||||
|
try {
|
||||||
|
await _equipmentCollection.doc(equipmentId).update({
|
||||||
|
'status': equipmentStatusToString(status),
|
||||||
|
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print('Error updating equipment status: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupérer un événement
|
||||||
|
Future<EventModel?> _getEvent(String eventId) async {
|
||||||
|
try {
|
||||||
|
final doc = await _eventsCollection.doc(eventId).get();
|
||||||
|
if (doc.exists) {
|
||||||
|
return EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error getting event: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ajouter un équipement à un événement
|
||||||
|
Future<void> addEquipmentToEvent(
|
||||||
|
String eventId,
|
||||||
|
String equipmentId, {
|
||||||
|
int quantity = 1,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final event = await _getEvent(eventId);
|
||||||
|
if (event == null) {
|
||||||
|
throw Exception('Event not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que l'équipement n'est pas déjà ajouté
|
||||||
|
final alreadyAdded = event.assignedEquipment.any((eq) => eq.equipmentId == equipmentId);
|
||||||
|
if (alreadyAdded) {
|
||||||
|
throw Exception('Equipment already added to event');
|
||||||
|
}
|
||||||
|
|
||||||
|
final newEquipment = EventEquipment(
|
||||||
|
equipmentId: equipmentId,
|
||||||
|
quantity: quantity,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedEquipment = [...event.assignedEquipment, newEquipment];
|
||||||
|
|
||||||
|
await _eventsCollection.doc(eventId).update({
|
||||||
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Décrémenter le stock pour les consommables
|
||||||
|
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
|
||||||
|
if (equipmentDoc.exists) {
|
||||||
|
final equipmentData = EquipmentModel.fromMap(
|
||||||
|
equipmentDoc.data() as Map<String, dynamic>,
|
||||||
|
equipmentDoc.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (equipmentData.hasQuantity) {
|
||||||
|
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
||||||
|
await _equipmentCollection.doc(equipmentId).update({
|
||||||
|
'availableQuantity': currentAvailable - quantity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error adding equipment to event: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retirer un équipement d'un événement
|
||||||
|
Future<void> removeEquipmentFromEvent(String eventId, String equipmentId) async {
|
||||||
|
try {
|
||||||
|
final event = await _getEvent(eventId);
|
||||||
|
if (event == null) {
|
||||||
|
throw Exception('Event not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
final equipmentToRemove = event.assignedEquipment.firstWhere(
|
||||||
|
(eq) => eq.equipmentId == equipmentId,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedEquipment = event.assignedEquipment
|
||||||
|
.where((eq) => eq.equipmentId != equipmentId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
await _eventsCollection.doc(eventId).update({
|
||||||
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restaurer le stock pour les consommables
|
||||||
|
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
|
||||||
|
if (equipmentDoc.exists) {
|
||||||
|
final equipmentData = EquipmentModel.fromMap(
|
||||||
|
equipmentDoc.data() as Map<String, dynamic>,
|
||||||
|
equipmentDoc.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (equipmentData.hasQuantity) {
|
||||||
|
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
||||||
|
await _equipmentCollection.doc(equipmentId).update({
|
||||||
|
'availableQuantity': currentAvailable + equipmentToRemove.quantity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error removing equipment from event: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
281
em2rp/lib/services/maintenance_service.dart
Normal file
281
em2rp/lib/services/maintenance_service.dart
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:em2rp/models/maintenance_model.dart';
|
||||||
|
import 'package:em2rp/models/alert_model.dart';
|
||||||
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
|
import 'package:em2rp/services/equipment_service.dart';
|
||||||
|
|
||||||
|
class MaintenanceService {
|
||||||
|
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||||
|
final EquipmentService _equipmentService = EquipmentService();
|
||||||
|
|
||||||
|
// Collection references
|
||||||
|
CollectionReference get _maintenancesCollection => _firestore.collection('maintenances');
|
||||||
|
CollectionReference get _equipmentCollection => _firestore.collection('equipment');
|
||||||
|
CollectionReference get _alertsCollection => _firestore.collection('alerts');
|
||||||
|
|
||||||
|
// CRUD Operations
|
||||||
|
|
||||||
|
/// Créer une nouvelle maintenance
|
||||||
|
Future<void> createMaintenance(MaintenanceModel maintenance) async {
|
||||||
|
try {
|
||||||
|
await _maintenancesCollection.doc(maintenance.id).set(maintenance.toMap());
|
||||||
|
|
||||||
|
// Mettre à jour les équipements concernés
|
||||||
|
for (String equipmentId in maintenance.equipmentIds) {
|
||||||
|
await _updateEquipmentMaintenanceList(equipmentId, maintenance.id);
|
||||||
|
|
||||||
|
// Si la maintenance est planifiée dans les 7 prochains jours, créer une alerte
|
||||||
|
if (maintenance.scheduledDate.isBefore(DateTime.now().add(const Duration(days: 7)))) {
|
||||||
|
await _createMaintenanceAlert(equipmentId, maintenance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error creating maintenance: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mettre à jour une maintenance
|
||||||
|
Future<void> updateMaintenance(String id, Map<String, dynamic> data) async {
|
||||||
|
try {
|
||||||
|
data['updatedAt'] = Timestamp.fromDate(DateTime.now());
|
||||||
|
await _maintenancesCollection.doc(id).update(data);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error updating maintenance: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supprimer une maintenance
|
||||||
|
Future<void> deleteMaintenance(String id) async {
|
||||||
|
try {
|
||||||
|
// Récupérer la maintenance pour connaître les équipements
|
||||||
|
final doc = await _maintenancesCollection.doc(id).get();
|
||||||
|
if (doc.exists) {
|
||||||
|
final maintenance = MaintenanceModel.fromMap(
|
||||||
|
doc.data() as Map<String, dynamic>,
|
||||||
|
doc.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Retirer la maintenance des équipements
|
||||||
|
for (String equipmentId in maintenance.equipmentIds) {
|
||||||
|
await _removeMaintenanceFromEquipment(equipmentId, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _maintenancesCollection.doc(id).delete();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error deleting maintenance: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupérer une maintenance par ID
|
||||||
|
Future<MaintenanceModel?> getMaintenanceById(String id) async {
|
||||||
|
try {
|
||||||
|
final doc = await _maintenancesCollection.doc(id).get();
|
||||||
|
if (doc.exists) {
|
||||||
|
return MaintenanceModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error getting maintenance: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupérer l'historique des maintenances pour un équipement
|
||||||
|
Stream<List<MaintenanceModel>> getMaintenances(String equipmentId) {
|
||||||
|
try {
|
||||||
|
return _maintenancesCollection
|
||||||
|
.where('equipmentIds', arrayContains: equipmentId)
|
||||||
|
.orderBy('scheduledDate', descending: true)
|
||||||
|
.snapshots()
|
||||||
|
.map((snapshot) {
|
||||||
|
return snapshot.docs
|
||||||
|
.map((doc) => MaintenanceModel.fromMap(
|
||||||
|
doc.data() as Map<String, dynamic>,
|
||||||
|
doc.id,
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print('Error streaming maintenances: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupérer toutes les maintenances
|
||||||
|
Stream<List<MaintenanceModel>> getAllMaintenances() {
|
||||||
|
try {
|
||||||
|
return _maintenancesCollection
|
||||||
|
.orderBy('scheduledDate', descending: true)
|
||||||
|
.snapshots()
|
||||||
|
.map((snapshot) {
|
||||||
|
return snapshot.docs
|
||||||
|
.map((doc) => MaintenanceModel.fromMap(
|
||||||
|
doc.data() as Map<String, dynamic>,
|
||||||
|
doc.id,
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print('Error streaming all maintenances: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marquer une maintenance comme complétée
|
||||||
|
Future<void> completeMaintenance(String id, {String? performedBy, double? cost}) async {
|
||||||
|
try {
|
||||||
|
final updateData = <String, dynamic>{
|
||||||
|
'completedDate': Timestamp.fromDate(DateTime.now()),
|
||||||
|
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (performedBy != null) {
|
||||||
|
updateData['performedBy'] = performedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cost != null) {
|
||||||
|
updateData['cost'] = cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateMaintenance(id, updateData);
|
||||||
|
|
||||||
|
// Mettre à jour la date de dernière maintenance des équipements
|
||||||
|
final maintenance = await getMaintenanceById(id);
|
||||||
|
if (maintenance != null) {
|
||||||
|
for (String equipmentId in maintenance.equipmentIds) {
|
||||||
|
await _equipmentCollection.doc(equipmentId).update({
|
||||||
|
'lastMaintenanceDate': Timestamp.fromDate(DateTime.now()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error completing maintenance: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifier les maintenances à venir et créer des alertes
|
||||||
|
Future<void> checkUpcomingMaintenances() async {
|
||||||
|
try {
|
||||||
|
final sevenDaysFromNow = DateTime.now().add(const Duration(days: 7));
|
||||||
|
|
||||||
|
// Récupérer les maintenances planifiées dans les 7 prochains jours
|
||||||
|
final maintenancesQuery = await _maintenancesCollection
|
||||||
|
.where('scheduledDate', isLessThanOrEqualTo: Timestamp.fromDate(sevenDaysFromNow))
|
||||||
|
.where('completedDate', isNull: true)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
for (var doc in maintenancesQuery.docs) {
|
||||||
|
final maintenance = MaintenanceModel.fromMap(
|
||||||
|
doc.data() as Map<String, dynamic>,
|
||||||
|
doc.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (String equipmentId in maintenance.equipmentIds) {
|
||||||
|
await _createMaintenanceAlert(equipmentId, maintenance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error checking upcoming maintenances: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Créer une alerte de maintenance à venir
|
||||||
|
Future<void> _createMaintenanceAlert(String equipmentId, MaintenanceModel maintenance) async {
|
||||||
|
try {
|
||||||
|
// Vérifier si une alerte existe déjà
|
||||||
|
final existingAlerts = await _alertsCollection
|
||||||
|
.where('equipmentId', isEqualTo: equipmentId)
|
||||||
|
.where('type', isEqualTo: alertTypeToString(AlertType.maintenanceDue))
|
||||||
|
.where('isRead', isEqualTo: false)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
// Vérifier si l'alerte concerne la même maintenance
|
||||||
|
bool alertExists = false;
|
||||||
|
for (var alertDoc in existingAlerts.docs) {
|
||||||
|
final alertData = alertDoc.data() as Map<String, dynamic>;
|
||||||
|
if (alertData['message']?.contains(maintenance.name) ?? false) {
|
||||||
|
alertExists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alertExists) {
|
||||||
|
// Récupérer l'équipement pour le nom
|
||||||
|
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
|
||||||
|
String equipmentName = equipmentId;
|
||||||
|
if (equipmentDoc.exists) {
|
||||||
|
final equipmentData = equipmentDoc.data() as Map<String, dynamic>;
|
||||||
|
equipmentName = equipmentData['name'] ?? equipmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
final daysUntil = maintenance.scheduledDate.difference(DateTime.now()).inDays;
|
||||||
|
final alert = AlertModel(
|
||||||
|
id: _alertsCollection.doc().id,
|
||||||
|
type: AlertType.maintenanceDue,
|
||||||
|
message: 'Maintenance "${maintenance.name}" prévue dans $daysUntil jour(s) pour $equipmentName',
|
||||||
|
equipmentId: equipmentId,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await _alertsCollection.doc(alert.id).set(alert.toMap());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error creating maintenance alert: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mettre à jour la liste des maintenances d'un équipement
|
||||||
|
Future<void> _updateEquipmentMaintenanceList(String equipmentId, String maintenanceId) async {
|
||||||
|
try {
|
||||||
|
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
|
||||||
|
if (equipmentDoc.exists) {
|
||||||
|
final equipment = EquipmentModel.fromMap(
|
||||||
|
equipmentDoc.data() as Map<String, dynamic>,
|
||||||
|
equipmentDoc.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedMaintenanceIds = List<String>.from(equipment.maintenanceIds);
|
||||||
|
if (!updatedMaintenanceIds.contains(maintenanceId)) {
|
||||||
|
updatedMaintenanceIds.add(maintenanceId);
|
||||||
|
|
||||||
|
await _equipmentCollection.doc(equipmentId).update({
|
||||||
|
'maintenanceIds': updatedMaintenanceIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error updating equipment maintenance list: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retirer une maintenance de la liste d'un équipement
|
||||||
|
Future<void> _removeMaintenanceFromEquipment(String equipmentId, String maintenanceId) async {
|
||||||
|
try {
|
||||||
|
final equipmentDoc = await _equipmentCollection.doc(equipmentId).get();
|
||||||
|
if (equipmentDoc.exists) {
|
||||||
|
final equipment = EquipmentModel.fromMap(
|
||||||
|
equipmentDoc.data() as Map<String, dynamic>,
|
||||||
|
equipmentDoc.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedMaintenanceIds = List<String>.from(equipment.maintenanceIds);
|
||||||
|
updatedMaintenanceIds.remove(maintenanceId);
|
||||||
|
|
||||||
|
await _equipmentCollection.doc(equipmentId).update({
|
||||||
|
'maintenanceIds': updatedMaintenanceIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error removing maintenance from equipment: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
133
em2rp/lib/views/equipment_form/brand_model_selector.dart
Normal file
133
em2rp/lib/views/equipment_form/brand_model_selector.dart
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:em2rp/providers/equipment_provider.dart';
|
||||||
|
|
||||||
|
class BrandModelSelector extends StatefulWidget {
|
||||||
|
final TextEditingController brandController;
|
||||||
|
final TextEditingController modelController;
|
||||||
|
final ValueChanged<String?>? onBrandChanged;
|
||||||
|
final List<String> filteredModels;
|
||||||
|
final String? selectedBrand;
|
||||||
|
final Function(List<String>) onModelsChanged;
|
||||||
|
|
||||||
|
const BrandModelSelector({
|
||||||
|
super.key,
|
||||||
|
required this.brandController,
|
||||||
|
required this.modelController,
|
||||||
|
this.onBrandChanged,
|
||||||
|
required this.filteredModels,
|
||||||
|
required this.selectedBrand,
|
||||||
|
required this.onModelsChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BrandModelSelector> createState() => _BrandModelSelectorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BrandModelSelectorState extends State<BrandModelSelector> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Consumer<EquipmentProvider>(
|
||||||
|
builder: (context, provider, child) {
|
||||||
|
return Autocomplete<String>(
|
||||||
|
initialValue: TextEditingValue(text: widget.brandController.text),
|
||||||
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||||
|
if (textEditingValue.text.isEmpty) {
|
||||||
|
return provider.brands;
|
||||||
|
}
|
||||||
|
return provider.brands.where((String brand) {
|
||||||
|
return brand.toLowerCase().contains(
|
||||||
|
textEditingValue.text.toLowerCase(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSelected: (String selection) async {
|
||||||
|
widget.brandController.text = selection;
|
||||||
|
widget.onBrandChanged?.call(selection);
|
||||||
|
final equipmentProvider = Provider.of<EquipmentProvider>(context, listen: false);
|
||||||
|
final models = await equipmentProvider.loadModelsByBrand(selection);
|
||||||
|
widget.onModelsChanged(models);
|
||||||
|
},
|
||||||
|
fieldViewBuilder: (context, controller, focusNode, onEditingComplete) {
|
||||||
|
if (controller.text != widget.brandController.text) {
|
||||||
|
controller.text = widget.brandController.text;
|
||||||
|
}
|
||||||
|
return TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Marque *',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.business),
|
||||||
|
helperText: 'Champ obligatoire',
|
||||||
|
),
|
||||||
|
onChanged: (value) async {
|
||||||
|
widget.brandController.text = value;
|
||||||
|
widget.modelController.clear();
|
||||||
|
widget.onBrandChanged?.call(value.isNotEmpty ? value : null);
|
||||||
|
if (value.isNotEmpty) {
|
||||||
|
final equipmentProvider = Provider.of<EquipmentProvider>(context, listen: false);
|
||||||
|
final models = await equipmentProvider.loadModelsByBrand(value);
|
||||||
|
widget.onModelsChanged(models);
|
||||||
|
} else {
|
||||||
|
widget.onModelsChanged([]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Autocomplete<String>(
|
||||||
|
initialValue: TextEditingValue(text: widget.modelController.text),
|
||||||
|
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||||
|
if (widget.selectedBrand == null || widget.selectedBrand!.isEmpty) {
|
||||||
|
return const Iterable<String>.empty();
|
||||||
|
}
|
||||||
|
if (textEditingValue.text.isEmpty) {
|
||||||
|
return widget.filteredModels;
|
||||||
|
}
|
||||||
|
return widget.filteredModels.where((String model) {
|
||||||
|
return model.toLowerCase().contains(
|
||||||
|
textEditingValue.text.toLowerCase(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSelected: (String selection) {
|
||||||
|
widget.modelController.text = selection;
|
||||||
|
},
|
||||||
|
fieldViewBuilder: (context, controller, focusNode, onEditingComplete) {
|
||||||
|
if (controller.text != widget.modelController.text) {
|
||||||
|
controller.text = widget.modelController.text;
|
||||||
|
}
|
||||||
|
return TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
enabled: widget.selectedBrand != null && widget.selectedBrand!.isNotEmpty,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Modèle *',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: const Icon(Icons.inventory_2),
|
||||||
|
hintText: widget.selectedBrand == null || widget.selectedBrand!.isEmpty
|
||||||
|
? 'Marque requise'
|
||||||
|
: 'Saisissez le modèle',
|
||||||
|
helperText: 'Champ obligatoire',
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
widget.modelController.text = value;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
26
em2rp/lib/views/equipment_form/id_generator.dart
Normal file
26
em2rp/lib/views/equipment_form/id_generator.dart
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:em2rp/services/equipment_service.dart';
|
||||||
|
|
||||||
|
class EquipmentIdGenerator {
|
||||||
|
static String generate({required String brand, required String model, int? number}) {
|
||||||
|
final brandTrim = brand.trim().replaceAll(' ', '_');
|
||||||
|
final modelTrim = model.trim().replaceAll(' ', '_');
|
||||||
|
if (brandTrim.isEmpty && modelTrim.isEmpty) {
|
||||||
|
return 'EQ-${DateTime.now().millisecondsSinceEpoch}${number != null ? '_$number' : ''}';
|
||||||
|
}
|
||||||
|
final brandPrefix = brandTrim.length >= 4 ? brandTrim.substring(0, 4) : brandTrim;
|
||||||
|
String baseId = modelTrim.isNotEmpty ? '${brandPrefix}_$modelTrim' : (brandPrefix.isNotEmpty ? brandPrefix : 'EQ');
|
||||||
|
if (number != null) {
|
||||||
|
baseId += '_#$number';
|
||||||
|
}
|
||||||
|
return baseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<String> ensureUniqueId(String baseId, EquipmentService service) async {
|
||||||
|
if (await service.isIdUnique(baseId)) {
|
||||||
|
return baseId;
|
||||||
|
}
|
||||||
|
return '${baseId}_${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
691
em2rp/lib/views/equipment_form_page.dart
Normal file
691
em2rp/lib/views/equipment_form_page.dart
Normal file
@@ -0,0 +1,691 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
|
import 'package:em2rp/providers/equipment_provider.dart';
|
||||||
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
|
import 'package:em2rp/services/equipment_service.dart';
|
||||||
|
import 'package:em2rp/utils/colors.dart';
|
||||||
|
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:em2rp/views/equipment_form/brand_model_selector.dart';
|
||||||
|
import 'package:em2rp/views/equipment_form/id_generator.dart';
|
||||||
|
|
||||||
|
class EquipmentFormPage extends StatefulWidget {
|
||||||
|
final EquipmentModel? equipment;
|
||||||
|
|
||||||
|
const EquipmentFormPage({super.key, this.equipment});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EquipmentFormPage> createState() => _EquipmentFormPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final EquipmentService _equipmentService = EquipmentService();
|
||||||
|
|
||||||
|
// Controllers
|
||||||
|
final TextEditingController _identifierController = TextEditingController();
|
||||||
|
final TextEditingController _brandController = TextEditingController();
|
||||||
|
final TextEditingController _modelController = TextEditingController();
|
||||||
|
final TextEditingController _purchasePriceController = TextEditingController();
|
||||||
|
final TextEditingController _rentalPriceController = TextEditingController();
|
||||||
|
final TextEditingController _totalQuantityController = TextEditingController();
|
||||||
|
final TextEditingController _criticalThresholdController = TextEditingController();
|
||||||
|
final TextEditingController _notesController = TextEditingController();
|
||||||
|
final TextEditingController _quantityToAddController = TextEditingController(text: '1');
|
||||||
|
|
||||||
|
// State variables
|
||||||
|
EquipmentCategory _selectedCategory = EquipmentCategory.other;
|
||||||
|
EquipmentStatus _selectedStatus = EquipmentStatus.available;
|
||||||
|
DateTime? _purchaseDate;
|
||||||
|
DateTime? _lastMaintenanceDate;
|
||||||
|
DateTime? _nextMaintenanceDate;
|
||||||
|
List<String> _selectedParentBoxIds = [];
|
||||||
|
List<EquipmentModel> _availableBoxes = [];
|
||||||
|
bool _isLoading = false;
|
||||||
|
bool _isLoadingBoxes = true;
|
||||||
|
bool _addMultiple = false;
|
||||||
|
String? _selectedBrand;
|
||||||
|
List<String> _filteredModels = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadAvailableBoxes();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final provider = Provider.of<EquipmentProvider>(context, listen: false);
|
||||||
|
provider.loadBrands();
|
||||||
|
provider.loadModels();
|
||||||
|
});
|
||||||
|
if (widget.equipment != null) {
|
||||||
|
_populateFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _populateFields() {
|
||||||
|
final equipment = widget.equipment!;
|
||||||
|
_identifierController.text = equipment.id;
|
||||||
|
_brandController.text = equipment.brand ?? '';
|
||||||
|
_selectedBrand = equipment.brand;
|
||||||
|
_modelController.text = equipment.model ?? '';
|
||||||
|
_selectedCategory = equipment.category;
|
||||||
|
_selectedStatus = equipment.status;
|
||||||
|
_purchasePriceController.text = equipment.purchasePrice?.toStringAsFixed(2) ?? '';
|
||||||
|
_rentalPriceController.text = equipment.rentalPrice?.toStringAsFixed(2) ?? '';
|
||||||
|
_totalQuantityController.text = equipment.totalQuantity?.toString() ?? '';
|
||||||
|
_criticalThresholdController.text = equipment.criticalThreshold?.toString() ?? '';
|
||||||
|
_purchaseDate = equipment.purchaseDate;
|
||||||
|
_lastMaintenanceDate = equipment.lastMaintenanceDate;
|
||||||
|
_nextMaintenanceDate = equipment.nextMaintenanceDate;
|
||||||
|
_selectedParentBoxIds = List.from(equipment.parentBoxIds);
|
||||||
|
_notesController.text = equipment.notes ?? '';
|
||||||
|
|
||||||
|
if (_selectedBrand != null && _selectedBrand!.isNotEmpty) {
|
||||||
|
_loadFilteredModels(_selectedBrand!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadAvailableBoxes() async {
|
||||||
|
try {
|
||||||
|
final boxes = await _equipmentService.getBoxes();
|
||||||
|
setState(() {
|
||||||
|
_availableBoxes = boxes;
|
||||||
|
_isLoadingBoxes = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_isLoadingBoxes = false;
|
||||||
|
});
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Erreur lors du chargement des boîtes : $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadFilteredModels(String brand) async {
|
||||||
|
try {
|
||||||
|
final equipmentProvider = Provider.of<EquipmentProvider>(context, listen: false);
|
||||||
|
final models = await equipmentProvider.loadModelsByBrand(brand);
|
||||||
|
setState(() {
|
||||||
|
_filteredModels = models;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_filteredModels = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_identifierController.dispose();
|
||||||
|
_brandController.dispose();
|
||||||
|
_modelController.dispose();
|
||||||
|
_purchasePriceController.dispose();
|
||||||
|
_rentalPriceController.dispose();
|
||||||
|
_totalQuantityController.dispose();
|
||||||
|
_criticalThresholdController.dispose();
|
||||||
|
_notesController.dispose();
|
||||||
|
_quantityToAddController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _isConsumable => _selectedCategory == EquipmentCategory.consumable || _selectedCategory == EquipmentCategory.cable;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final localUserProvider = Provider.of<LocalUserProvider>(context);
|
||||||
|
final hasManagePermission = localUserProvider.hasPermission('manage_equipment');
|
||||||
|
final isEditing = widget.equipment != null;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: CustomAppBar(
|
||||||
|
title: isEditing ? 'Modifier l\'équipement' : 'Nouvel équipement',
|
||||||
|
),
|
||||||
|
body: _isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Identifiant (généré ou saisi)
|
||||||
|
TextFormField(
|
||||||
|
controller: _identifierController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Identifiant *',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: const Icon(Icons.tag),
|
||||||
|
hintText: isEditing ? null : 'Laissez vide pour générer automatiquement',
|
||||||
|
helperText: isEditing ? 'Non modifiable' : 'Format auto: {Marque4Chars}_{Modèle}',
|
||||||
|
),
|
||||||
|
enabled: !isEditing,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Case à cocher "Ajouter plusieurs" (uniquement en mode création)
|
||||||
|
if (!isEditing) ...[
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: CheckboxListTile(
|
||||||
|
title: const Text('Ajouter plusieurs équipements'),
|
||||||
|
subtitle: const Text('Créer plusieurs équipements numérotés'),
|
||||||
|
value: _addMultiple,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
onChanged: (bool? value) {
|
||||||
|
setState(() {
|
||||||
|
_addMultiple = value ?? false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_addMultiple) ...[
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _quantityToAddController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Quantité ou range',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.plus_one),
|
||||||
|
hintText: '5 ou 6-18',
|
||||||
|
helperText: 'Ex: 5 ou 6-18',
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
validator: (value) {
|
||||||
|
if (_addMultiple) {
|
||||||
|
if (value == null || value.isEmpty) return 'Requis';
|
||||||
|
// Vérifier si c'est un nombre simple ou une range
|
||||||
|
if (value.contains('-')) {
|
||||||
|
final parts = value.split('-');
|
||||||
|
if (parts.length != 2) return 'Format invalide';
|
||||||
|
final start = int.tryParse(parts[0].trim());
|
||||||
|
final end = int.tryParse(parts[1].trim());
|
||||||
|
if (start == null || end == null) return 'Nombres invalides';
|
||||||
|
if (start >= end) return 'Le début doit être < fin';
|
||||||
|
if (end - start > 100) return 'Max 100 équipements';
|
||||||
|
} else {
|
||||||
|
final num = int.tryParse(value);
|
||||||
|
if (num == null || num < 1 || num > 100) return '1-100';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Sélecteur Marque/Modèle
|
||||||
|
BrandModelSelector(
|
||||||
|
brandController: _brandController,
|
||||||
|
modelController: _modelController,
|
||||||
|
selectedBrand: _selectedBrand,
|
||||||
|
filteredModels: _filteredModels,
|
||||||
|
onBrandChanged: (brand) {
|
||||||
|
setState(() {
|
||||||
|
_selectedBrand = brand;
|
||||||
|
});
|
||||||
|
if (brand != null && brand.isNotEmpty) {
|
||||||
|
_loadFilteredModels(brand);
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_filteredModels = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onModelsChanged: (models) {
|
||||||
|
setState(() {
|
||||||
|
_filteredModels = models;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Catégorie et Statut
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: DropdownButtonFormField<EquipmentCategory>(
|
||||||
|
value: _selectedCategory,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Catégorie *',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.category),
|
||||||
|
),
|
||||||
|
items: EquipmentCategory.values.map((category) {
|
||||||
|
return DropdownMenuItem(
|
||||||
|
value: category,
|
||||||
|
child: Text(_getCategoryLabel(category)),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedCategory = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Afficher le statut uniquement si ce n'est pas un consommable ou câble
|
||||||
|
if (!_isConsumable) ...[
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: DropdownButtonFormField<EquipmentStatus>(
|
||||||
|
value: _selectedStatus,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Statut *',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.info),
|
||||||
|
),
|
||||||
|
items: EquipmentStatus.values.map((status) {
|
||||||
|
return DropdownMenuItem(
|
||||||
|
value: status,
|
||||||
|
child: Text(_getStatusLabel(status)),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedStatus = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Prix
|
||||||
|
if (hasManagePermission) ...[
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _purchasePriceController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Prix d\'achat (€)',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.euro),
|
||||||
|
),
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _rentalPriceController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Prix de location (€)',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.attach_money),
|
||||||
|
),
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Quantités pour consommables
|
||||||
|
if (_isConsumable) ...[
|
||||||
|
const Divider(),
|
||||||
|
const Text('Gestion des quantités', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _totalQuantityController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Quantité totale',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.inventory),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _criticalThresholdController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Seuil critique',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.warning),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Boîtes parentes
|
||||||
|
const Divider(),
|
||||||
|
const Text('Boîtes parentes', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_isLoadingBoxes
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: _buildParentBoxesSelector(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
const Divider(),
|
||||||
|
const Text('Dates', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildDateField(label: 'Date d\'achat', icon: Icons.shopping_cart, value: _purchaseDate, onTap: () => _selectDate(context, 'purchase')),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildDateField(label: 'Dernière maintenance', icon: Icons.build, value: _lastMaintenanceDate, onTap: () => _selectDate(context, 'lastMaintenance')),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildDateField(label: 'Prochaine maintenance', icon: Icons.event, value: _nextMaintenanceDate, onTap: () => _selectDate(context, 'nextMaintenance')),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Notes
|
||||||
|
const Divider(),
|
||||||
|
TextFormField(
|
||||||
|
controller: _notesController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Notes',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.notes),
|
||||||
|
),
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Boutons
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _saveEquipment,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.rouge,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||||
|
),
|
||||||
|
child: Text(isEditing ? 'Enregistrer' : 'Créer', style: const TextStyle(color: Colors.white)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildParentBoxesSelector() {
|
||||||
|
if (_availableBoxes.isEmpty) {
|
||||||
|
return const Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: Text('Aucune boîte disponible'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: Column(
|
||||||
|
children: _availableBoxes.map((box) {
|
||||||
|
final isSelected = _selectedParentBoxIds.contains(box.id);
|
||||||
|
return CheckboxListTile(
|
||||||
|
title: Text(box.name),
|
||||||
|
subtitle: box.model != null ? Text('Modèle: {box.model}') : null,
|
||||||
|
value: isSelected,
|
||||||
|
onChanged: (bool? value) {
|
||||||
|
setState(() {
|
||||||
|
if (value == true) {
|
||||||
|
_selectedParentBoxIds.add(box.id);
|
||||||
|
} else {
|
||||||
|
_selectedParentBoxIds.remove(box.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDateField({required String label, required IconData icon, required DateTime? value, required VoidCallback onTap}) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
child: InputDecorator(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(icon),
|
||||||
|
suffixIcon: value != null
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
if (label.contains('achat')) {
|
||||||
|
_purchaseDate = null;
|
||||||
|
} else if (label.contains('Dernière')) {
|
||||||
|
_lastMaintenanceDate = null;
|
||||||
|
} else if (label.contains('Prochaine')) {
|
||||||
|
_nextMaintenanceDate = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
value != null ? DateFormat('dd/MM/yyyy').format(value) : 'Sélectionner une date',
|
||||||
|
style: TextStyle(color: value != null ? Colors.black : Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _selectDate(BuildContext context, String field) async {
|
||||||
|
final DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: DateTime.now(),
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: DateTime(2100),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (picked != null) {
|
||||||
|
setState(() {
|
||||||
|
switch (field) {
|
||||||
|
case 'purchase':
|
||||||
|
_purchaseDate = picked;
|
||||||
|
break;
|
||||||
|
case 'lastMaintenance':
|
||||||
|
_lastMaintenanceDate = picked;
|
||||||
|
break;
|
||||||
|
case 'nextMaintenance':
|
||||||
|
_nextMaintenanceDate = picked;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveEquipment() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final equipmentProvider = Provider.of<EquipmentProvider>(context, listen: false);
|
||||||
|
final isEditing = widget.equipment != null;
|
||||||
|
|
||||||
|
int? availableQuantity;
|
||||||
|
if (_isConsumable && _totalQuantityController.text.isNotEmpty) {
|
||||||
|
final totalQuantity = int.parse(_totalQuantityController.text);
|
||||||
|
if (isEditing && widget.equipment!.availableQuantity != null) {
|
||||||
|
availableQuantity = widget.equipment!.availableQuantity;
|
||||||
|
} else {
|
||||||
|
availableQuantity = totalQuantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation marque/modèle obligatoires
|
||||||
|
String brand = _brandController.text.trim();
|
||||||
|
String model = _modelController.text.trim();
|
||||||
|
|
||||||
|
if (brand.isEmpty || model.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('La marque et le modèle sont obligatoires')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Génération d'identifiant si vide
|
||||||
|
List<String> ids = [];
|
||||||
|
List<int> numbers = [];
|
||||||
|
|
||||||
|
if (!isEditing && _identifierController.text.isEmpty) {
|
||||||
|
// Gérer la range ou nombre simple
|
||||||
|
final quantityText = _quantityToAddController.text.trim();
|
||||||
|
if (_addMultiple && quantityText.contains('-')) {
|
||||||
|
// Range: ex "6-18"
|
||||||
|
final parts = quantityText.split('-');
|
||||||
|
final start = int.parse(parts[0].trim());
|
||||||
|
final end = int.parse(parts[1].trim());
|
||||||
|
for (int i = start; i <= end; i++) {
|
||||||
|
numbers.add(i);
|
||||||
|
}
|
||||||
|
} else if (_addMultiple) {
|
||||||
|
// Nombre simple
|
||||||
|
final nbToAdd = int.tryParse(quantityText) ?? 1;
|
||||||
|
for (int i = 1; i <= nbToAdd; i++) {
|
||||||
|
numbers.add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Générer les IDs
|
||||||
|
if (numbers.isEmpty) {
|
||||||
|
String baseId = EquipmentIdGenerator.generate(brand: brand, model: model, number: null);
|
||||||
|
String uniqueId = await EquipmentIdGenerator.ensureUniqueId(baseId, _equipmentService);
|
||||||
|
ids.add(uniqueId);
|
||||||
|
} else {
|
||||||
|
for (final num in numbers) {
|
||||||
|
String baseId = EquipmentIdGenerator.generate(brand: brand, model: model, number: num);
|
||||||
|
String uniqueId = await EquipmentIdGenerator.ensureUniqueId(baseId, _equipmentService);
|
||||||
|
ids.add(uniqueId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ids.add(_identifierController.text.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Création des équipements
|
||||||
|
for (final id in ids) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final equipment = EquipmentModel(
|
||||||
|
id: id,
|
||||||
|
name: id, // Utilisation de l'identifiant comme nom
|
||||||
|
brand: brand,
|
||||||
|
model: model,
|
||||||
|
category: _selectedCategory,
|
||||||
|
status: _selectedStatus,
|
||||||
|
purchasePrice: _purchasePriceController.text.isNotEmpty ? double.tryParse(_purchasePriceController.text) : null,
|
||||||
|
rentalPrice: _rentalPriceController.text.isNotEmpty ? double.tryParse(_rentalPriceController.text) : null,
|
||||||
|
totalQuantity: _isConsumable ? int.tryParse(_totalQuantityController.text) : null,
|
||||||
|
criticalThreshold: _isConsumable ? int.tryParse(_criticalThresholdController.text) : null,
|
||||||
|
purchaseDate: _purchaseDate,
|
||||||
|
lastMaintenanceDate: _lastMaintenanceDate,
|
||||||
|
nextMaintenanceDate: _nextMaintenanceDate,
|
||||||
|
parentBoxIds: _selectedParentBoxIds,
|
||||||
|
notes: _notesController.text,
|
||||||
|
createdAt: isEditing ? (widget.equipment?.createdAt ?? now) : now,
|
||||||
|
updatedAt: now,
|
||||||
|
availableQuantity: availableQuantity,
|
||||||
|
);
|
||||||
|
if (isEditing) {
|
||||||
|
await equipmentProvider.updateEquipment(
|
||||||
|
equipment.id,
|
||||||
|
equipment.toMap(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await equipmentProvider.addEquipment(equipment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Erreur lors de l\'enregistrement : $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correction des enums dans _getCategoryLabel
|
||||||
|
String _getCategoryLabel(EquipmentCategory category) {
|
||||||
|
switch (category) {
|
||||||
|
case EquipmentCategory.lighting:
|
||||||
|
return 'Lumière';
|
||||||
|
case EquipmentCategory.sound:
|
||||||
|
return 'Son';
|
||||||
|
case EquipmentCategory.video:
|
||||||
|
return 'Vidéo';
|
||||||
|
case EquipmentCategory.effect:
|
||||||
|
return 'Effet';
|
||||||
|
case EquipmentCategory.structure:
|
||||||
|
return 'Structure';
|
||||||
|
case EquipmentCategory.consumable:
|
||||||
|
return 'Consommable';
|
||||||
|
case EquipmentCategory.cable:
|
||||||
|
return 'Câble';
|
||||||
|
case EquipmentCategory.other:
|
||||||
|
default:
|
||||||
|
return 'Autre';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correction des enums dans _getStatusLabel
|
||||||
|
String _getStatusLabel(EquipmentStatus status) {
|
||||||
|
switch (status) {
|
||||||
|
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';
|
||||||
|
default:
|
||||||
|
return 'Autre';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1474
em2rp/lib/views/equipment_management_page.dart
Normal file
1474
em2rp/lib/views/equipment_management_page.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import 'package:em2rp/views/calendar_page.dart';
|
|||||||
import 'package:em2rp/views/my_account_page.dart';
|
import 'package:em2rp/views/my_account_page.dart';
|
||||||
import 'package:em2rp/views/user_management_page.dart';
|
import 'package:em2rp/views/user_management_page.dart';
|
||||||
import 'package:em2rp/views/data_management_page.dart';
|
import 'package:em2rp/views/data_management_page.dart';
|
||||||
|
import 'package:em2rp/views/equipment_management_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:em2rp/views/widgets/image/profile_picture.dart';
|
import 'package:em2rp/views/widgets/image/profile_picture.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -151,6 +152,24 @@ class MainDrawer extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
PermissionGate(
|
||||||
|
requiredPermissions: const ['view_equipment'],
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(Icons.inventory),
|
||||||
|
title: const Text('Gestion du Matériel'),
|
||||||
|
selected: currentPage == '/equipment_management',
|
||||||
|
selectedColor: AppColors.rouge,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
const EquipmentManagementPage()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ dependencies:
|
|||||||
path_provider: ^2.1.2
|
path_provider: ^2.1.2
|
||||||
pdf: ^3.10.7
|
pdf: ^3.10.7
|
||||||
printing: ^5.11.1
|
printing: ^5.11.1
|
||||||
|
qr_flutter: ^4.1.0
|
||||||
flutter_local_notifications: ^19.2.1
|
flutter_local_notifications: ^19.2.1
|
||||||
timezone: ^0.10.1
|
timezone: ^0.10.1
|
||||||
flutter_secure_storage: ^9.0.0
|
flutter_secure_storage: ^9.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user