refactor: Remplacement de l'accès direct à Firestore par des Cloud Functions

Migration complète du backend pour utiliser des Cloud Functions comme couche API sécurisée, en remplacement des appels directs à Firestore depuis le client.

**Backend (Cloud Functions):**
- **Centralisation CORS :** Ajout d'un middleware `withCors` et d'une configuration `httpOptions` pour gérer uniformément les en-têtes CORS et les requêtes `OPTIONS` sur toutes les fonctions.
- **Nouvelles Fonctions de Lecture (GET) :**
    - `getEquipments`, `getContainers`, `getEvents`, `getUsers`, `getOptions`, `getEventTypes`, `getRoles`, `getMaintenances`, `getAlerts`.
    - Ces fonctions gèrent les permissions côté serveur, masquant les données sensibles (ex: prix des équipements) pour les utilisateurs non-autorisés.
    - `getEvents` retourne également une map des utilisateurs (`usersMap`) pour optimiser le chargement des données de la main d'œuvre.
- **Nouvelle Fonction de Recherche :**
    - `getContainersByEquipment` : Endpoint dédié pour trouver efficacement tous les containers qui contiennent un équipement spécifique.
- **Nouvelles Fonctions d'Écriture (CRUD) :**
    - Fonctions CRUD complètes pour `eventTypes` (`create`, `update`, `delete`), incluant la validation (unicité du nom, vérification des événements futurs avant suppression).
- **Mise à jour de Fonctions Existantes :**
    - Toutes les fonctions CRUD existantes (`create/update/deleteEquipment`, `create/update/deleteContainer`, etc.) sont wrappées avec le nouveau gestionnaire CORS.

**Frontend (Flutter):**
- **Introduction du `DataService` :** Nouveau service centralisant tous les appels aux Cloud Functions, servant d'intermédiaire entre l'UI/Providers et l'API.
- **Refactorisation des Providers :**
    - `EquipmentProvider`, `ContainerProvider`, `EventProvider`, `UsersProvider`, `MaintenanceProvider` et `AlertProvider` ont été refactorisés pour utiliser le `DataService` au lieu d'accéder directement à Firestore.
    - Les `Stream` Firestore sont remplacés par des chargements de données via des méthodes `Future` (`loadEquipments`, `loadEvents`, etc.).
- **Gestion des Relations Équipement-Container :**
    - Le modèle `EquipmentModel` ne stocke plus `parentBoxIds`.
    - La relation est maintenant gérée par le `ContainerModel` qui contient `equipmentIds`.
    - Le `ContainerEquipmentService` est introduit pour utiliser la nouvelle fonction `getContainersByEquipment`.
    - L'affichage des boîtes parentes (`EquipmentParentContainers`) et le formulaire d'équipement (`EquipmentFormPage`) ont été mis à jour pour refléter ce nouveau modèle de données, synchronisant les ajouts/suppressions d'équipements dans les containers.
- **Amélioration de l'UI :**
    - Nouveau widget `ParentBoxesSelector` pour une sélection améliorée et visuelle des boîtes parentes dans le formulaire d'équipement.
    - Refonte visuelle de `EquipmentParentContainers` pour une meilleure présentation.
This commit is contained in:
ElPoyo
2026-01-12 20:38:46 +01:00
parent 13a890606d
commit f38d75362c
46 changed files with 3367 additions and 1510 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -9,20 +9,60 @@ const admin = require('firebase-admin');
function serializeTimestamps(data) { function serializeTimestamps(data) {
if (!data) return data; if (!data) return data;
// Éviter la récursion sur les types Firestore spéciaux
if (data._firestore || data._path || data._converter) {
// C'est un objet Firestore interne, ne pas le traiter
if (data.id && data.path) {
// C'est une DocumentReference
return data.path;
}
return null;
}
const result = { ...data }; const result = { ...data };
for (const key in result) { for (const key in result) {
if (result[key] && result[key].toDate && typeof result[key].toDate === 'function') { const value = result[key];
// C'est un Timestamp Firestore
result[key] = result[key].toDate().toISOString(); if (!value) {
} else if (result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])) { continue;
// Objet imbriqué }
result[key] = serializeTimestamps(result[key]);
} else if (Array.isArray(result[key])) { // Gérer les Timestamps Firestore
// Tableau if (value.toDate && typeof value.toDate === 'function') {
result[key] = result[key].map(item => result[key] = value.toDate().toISOString();
item && typeof item === 'object' ? serializeTimestamps(item) : item }
); // Gérer les DocumentReference
else if (value.path && value.id && typeof value.path === 'string') {
result[key] = value.path;
}
// Gérer les GeoPoint
else if (value.latitude !== undefined && value.longitude !== undefined) {
result[key] = {
latitude: value.latitude,
longitude: value.longitude
};
}
// Gérer les tableaux
else if (Array.isArray(value)) {
result[key] = value.map(item => {
if (!item || typeof item !== 'object') return item;
// DocumentReference dans un tableau
if (item.path && item.id) {
return item.path;
}
// Timestamp dans un tableau
if (item.toDate && typeof item.toDate === 'function') {
return item.toDate().toISOString();
}
// Objet normal
return serializeTimestamps(item);
});
}
// Gérer les objets imbriqués (mais pas les objets Firestore)
else if (typeof value === 'object' && !value._firestore && !value._path) {
result[key] = serializeTimestamps(value);
} }
} }

View File

@@ -5,6 +5,8 @@ import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/models/event_type_model.dart'; import 'package:em2rp/models/event_type_model.dart';
import 'package:em2rp/models/user_model.dart'; import 'package:em2rp/models/user_model.dart';
import 'package:em2rp/services/event_form_service.dart'; import 'package:em2rp/services/event_form_service.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:em2rp/providers/event_provider.dart'; import 'package:em2rp/providers/event_provider.dart';
import 'package:em2rp/providers/local_user_provider.dart'; import 'package:em2rp/providers/local_user_provider.dart';
@@ -125,7 +127,14 @@ class EventFormController extends ChangeNotifier {
_assignedEquipment = List<EventEquipment>.from(event.assignedEquipment); _assignedEquipment = List<EventEquipment>.from(event.assignedEquipment);
_assignedContainers = List<String>.from(event.assignedContainers); _assignedContainers = List<String>.from(event.assignedContainers);
_selectedEventTypeId = event.eventTypeId.isNotEmpty ? event.eventTypeId : null; _selectedEventTypeId = event.eventTypeId.isNotEmpty ? event.eventTypeId : null;
_selectedUserIds = event.workforce.map((ref) => ref.id).toList();
// Gérer workforce qui peut contenir String ou DocumentReference
_selectedUserIds = event.workforce.map((ref) {
if (ref is String) return ref;
if (ref is DocumentReference) return ref.id;
return '';
}).where((id) => id.isNotEmpty).toList();
_uploadedFiles = List<Map<String, String>>.from(event.documents); _uploadedFiles = List<Map<String, String>>.from(event.documents);
_selectedOptions = List<Map<String, dynamic>>.from(event.options); _selectedOptions = List<Map<String, dynamic>>.from(event.options);
_selectedStatus = event.status; _selectedStatus = event.status;
@@ -422,8 +431,9 @@ class EventFormController extends ChangeNotifier {
notifyListeners(); notifyListeners();
try { try {
// Supprimer l'événement de Firestore // Supprimer l'événement via l'API
await FirebaseFirestore.instance.collection('events').doc(eventId).delete(); final dataService = DataService(FirebaseFunctionsApiService());
await dataService.deleteEvent(eventId);
// Recharger la liste des événements // Recharger la liste des événements
final localUserProvider = Provider.of<LocalUserProvider>(context, listen: false); final localUserProvider = Provider.of<LocalUserProvider>(context, listen: false);

View File

@@ -54,30 +54,31 @@ void main() async {
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: [
// Injection du service UserService
Provider<UserService>(create: (_) => UserService()),
// LocalUserProvider pour la gestion de l'authentification // LocalUserProvider pour la gestion de l'authentification
ChangeNotifierProvider<LocalUserProvider>( ChangeNotifierProvider<LocalUserProvider>(
create: (context) => LocalUserProvider()), create: (context) => LocalUserProvider()),
// Injection des Providers en utilisant UserService // UsersProvider migré vers l'API
ChangeNotifierProvider<UsersProvider>( ChangeNotifierProvider<UsersProvider>(
create: (context) => UsersProvider(context.read<UserService>()), create: (context) => UsersProvider(),
), ),
// EventProvider pour la gestion des événements // EventProvider migré vers l'API
ChangeNotifierProvider<EventProvider>( ChangeNotifierProvider<EventProvider>(
create: (context) => EventProvider(), create: (context) => EventProvider(),
), ),
// Providers pour la gestion du matériel // EquipmentProvider migré vers l'API
ChangeNotifierProvider<EquipmentProvider>( ChangeNotifierProvider<EquipmentProvider>(
create: (context) => EquipmentProvider(), create: (context) => EquipmentProvider(),
), ),
// ContainerProvider migré vers l'API
ChangeNotifierProvider<ContainerProvider>( ChangeNotifierProvider<ContainerProvider>(
create: (context) => ContainerProvider(), create: (context) => ContainerProvider(),
), ),
// MaintenanceProvider migré vers l'API
ChangeNotifierProvider<MaintenanceProvider>( ChangeNotifierProvider<MaintenanceProvider>(
create: (context) => MaintenanceProvider(), create: (context) => MaintenanceProvider(),
), ),

View File

@@ -3,7 +3,8 @@
enum AlertType { enum AlertType {
lowStock, // Stock faible lowStock, // Stock faible
maintenanceDue, // Maintenance à venir maintenanceDue, // Maintenance à venir
conflict // Conflit disponibilité conflict, // Conflit disponibilité
lost // Équipement perdu
} }
String alertTypeToString(AlertType type) { String alertTypeToString(AlertType type) {
@@ -14,6 +15,8 @@ String alertTypeToString(AlertType type) {
return 'MAINTENANCE_DUE'; return 'MAINTENANCE_DUE';
case AlertType.conflict: case AlertType.conflict:
return 'CONFLICT'; return 'CONFLICT';
case AlertType.lost:
return 'LOST';
} }
} }
@@ -25,6 +28,8 @@ AlertType alertTypeFromString(String? type) {
return AlertType.maintenanceDue; return AlertType.maintenanceDue;
case 'CONFLICT': case 'CONFLICT':
return AlertType.conflict; return AlertType.conflict;
case 'LOST':
return AlertType.lost;
default: default:
return AlertType.conflict; return AlertType.conflict;
} }

View File

@@ -250,7 +250,7 @@ class EventModel {
final String address; final String address;
final double latitude; final double latitude;
final double longitude; final double longitude;
final List<DocumentReference> workforce; final List<dynamic> workforce; // Peut contenir DocumentReference OU String (UIDs)
final List<Map<String, String>> documents; final List<Map<String, String>> documents;
final List<Map<String, dynamic>> options; final List<Map<String, dynamic>> options;
final EventStatus status; final EventStatus status;
@@ -310,11 +310,14 @@ class EventModel {
// Gestion sécurisée des références workforce // Gestion sécurisée des références workforce
final List<dynamic> workforceRefs = map['workforce'] ?? []; final List<dynamic> workforceRefs = map['workforce'] ?? [];
final List<DocumentReference> safeWorkforce = []; final List<dynamic> safeWorkforce = [];
for (var ref in workforceRefs) { for (var ref in workforceRefs) {
if (ref is DocumentReference) { if (ref is DocumentReference) {
safeWorkforce.add(ref); safeWorkforce.add(ref);
} else if (ref is String) {
// Accepter directement les UIDs (envoyés par le backend)
safeWorkforce.add(ref);
} else { } else {
print('Warning: Invalid workforce reference in event $id: $ref'); print('Warning: Invalid workforce reference in event $id: $ref');
} }
@@ -527,7 +530,7 @@ class EventModel {
String? address, String? address,
double? latitude, double? latitude,
double? longitude, double? longitude,
List<DocumentReference>? workforce, List<dynamic>? workforce,
List<Map<String, String>>? documents, List<Map<String, String>>? documents,
List<Map<String, dynamic>>? options, List<Map<String, dynamic>>? options,
EventStatus? status, EventStatus? status,

View File

@@ -1,3 +1,5 @@
import 'package:cloud_firestore/cloud_firestore.dart';
class EventTypeModel { class EventTypeModel {
final String id; final String id;
final String name; final String name;
@@ -12,11 +14,19 @@ class EventTypeModel {
}); });
factory EventTypeModel.fromMap(Map<String, dynamic> map, String id) { factory EventTypeModel.fromMap(Map<String, dynamic> map, String id) {
// Gérer createdAt qui peut être Timestamp (Firestore) ou String ISO (API)
DateTime parseCreatedAt(dynamic value) {
if (value == null) return DateTime.now();
if (value is Timestamp) return value.toDate();
if (value is String) return DateTime.tryParse(value) ?? DateTime.now();
return DateTime.now();
}
return EventTypeModel( return EventTypeModel(
id: id, id: id,
name: map['name'] ?? '', name: map['name'] ?? '',
defaultPrice: (map['defaultPrice'] ?? 0.0).toDouble(), defaultPrice: (map['defaultPrice'] ?? 0.0).toDouble(),
createdAt: map['createdAt']?.toDate() ?? DateTime.now(), createdAt: parseCreatedAt(map['createdAt']),
); );
} }

View File

@@ -0,0 +1,62 @@
import 'package:flutter/foundation.dart';
import 'package:em2rp/models/alert_model.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
class AlertProvider extends ChangeNotifier {
final DataService _dataService = DataService(FirebaseFunctionsApiService());
List<AlertModel> _alerts = [];
bool _isLoading = false;
List<AlertModel> get alerts => _alerts;
bool get isLoading => _isLoading;
/// Nombre d'alertes non lues
int get unreadCount => _alerts.where((a) => !a.isRead).length;
/// Charger toutes les alertes via l'API
Future<void> loadAlerts() async {
_isLoading = true;
notifyListeners();
try {
final alertsData = await _dataService.getAlerts();
_alerts = alertsData.map((data) {
return AlertModel.fromMap(data, data['id'] as String);
}).toList();
_isLoading = false;
notifyListeners();
} catch (e) {
print('Error loading alerts: $e');
_isLoading = false;
notifyListeners();
rethrow;
}
}
/// Recharger les alertes
Future<void> refresh() async {
await loadAlerts();
}
/// Obtenir les alertes non lues
List<AlertModel> get unreadAlerts {
return _alerts.where((a) => !a.isRead).toList();
}
/// Obtenir les alertes par type
List<AlertModel> getByType(AlertType type) {
return _alerts.where((a) => a.type == type).toList();
}
/// Obtenir les alertes critiques (stock bas, équipement perdu)
List<AlertModel> get criticalAlerts {
return _alerts.where((a) =>
a.type == AlertType.lowStock || a.type == AlertType.lost
).toList();
}
}

View File

@@ -6,13 +6,40 @@ import 'package:em2rp/services/container_service.dart';
class ContainerProvider with ChangeNotifier { class ContainerProvider with ChangeNotifier {
final ContainerService _containerService = ContainerService(); final ContainerService _containerService = ContainerService();
List<ContainerModel> _containers = [];
ContainerType? _selectedType; ContainerType? _selectedType;
EquipmentStatus? _selectedStatus; EquipmentStatus? _selectedStatus;
String _searchQuery = ''; String _searchQuery = '';
bool _isLoading = false;
List<ContainerModel> get containers => _containers;
ContainerType? get selectedType => _selectedType; ContainerType? get selectedType => _selectedType;
EquipmentStatus? get selectedStatus => _selectedStatus; EquipmentStatus? get selectedStatus => _selectedStatus;
String get searchQuery => _searchQuery; String get searchQuery => _searchQuery;
bool get isLoading => _isLoading;
/// Charger tous les containers via l'API
Future<void> loadContainers() async {
_isLoading = true;
notifyListeners();
try {
// Pour l'instant, on écoute le stream et on garde la première valeur
_containerService.getContainers(
type: _selectedType,
status: _selectedStatus,
searchQuery: _searchQuery,
).listen((containers) {
_containers = containers;
_isLoading = false;
notifyListeners();
});
} catch (e) {
print('Error loading containers: $e');
_isLoading = false;
notifyListeners();
}
}
/// Stream des containers avec filtres appliqués /// Stream des containers avec filtres appliqués
Stream<List<ContainerModel>> get containersStream { Stream<List<ContainerModel>> get containersStream {

View File

@@ -0,0 +1,109 @@
import 'package:flutter/foundation.dart';
import 'package:em2rp/models/container_model.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
import '../models/equipment_model.dart';
class ContainerProvider extends ChangeNotifier {
final DataService _dataService = DataService(FirebaseFunctionsApiService());
List<ContainerModel> _containers = [];
ContainerType? _selectedType;
EquipmentStatus? _selectedStatus;
String _searchQuery = '';
bool _isLoading = false;
// Getters
List<ContainerModel> get containers => _filteredContainers;
ContainerType? get selectedType => _selectedType;
EquipmentStatus? get selectedStatus => _selectedStatus;
String get searchQuery => _searchQuery;
bool get isLoading => _isLoading;
/// Charger tous les conteneurs via l'API
Future<void> loadContainers() async {
_isLoading = true;
notifyListeners();
try {
final containersData = await _dataService.getContainers();
_containers = containersData.map((data) {
return ContainerModel.fromMap(data, data['id'] as String);
}).toList();
_isLoading = false;
notifyListeners();
} catch (e) {
print('Error loading containers: $e');
_isLoading = false;
notifyListeners();
rethrow;
}
}
/// Obtenir les conteneurs filtrés
List<ContainerModel> get _filteredContainers {
var filtered = _containers;
if (_selectedType != null) {
filtered = filtered.where((c) => c.type == _selectedType).toList();
}
if (_selectedStatus != null) {
filtered = filtered.where((c) => c.status == _selectedStatus).toList();
}
if (_searchQuery.isNotEmpty) {
final query = _searchQuery.toLowerCase();
filtered = filtered.where((c) {
return c.name.toLowerCase().contains(query) ||
c.id.toLowerCase().contains(query);
}).toList();
}
return filtered;
}
/// Définir le filtre de type
void setSelectedType(ContainerType? type) {
_selectedType = type;
notifyListeners();
}
/// Définir le filtre de statut
void setSelectedStatus(EquipmentStatus? status) {
_selectedStatus = status;
notifyListeners();
}
/// Définir la requête de recherche
void setSearchQuery(String query) {
_searchQuery = query;
notifyListeners();
}
/// Réinitialiser tous les filtres
void clearFilters() {
_selectedType = null;
_selectedStatus = null;
_searchQuery = '';
notifyListeners();
}
/// Recharger les conteneurs
Future<void> refresh() async {
await loadContainers();
}
/// Obtenir un conteneur par ID
ContainerModel? getById(String id) {
try {
return _containers.firstWhere((c) => c.id == id);
} catch (e) {
return null;
}
}
}

View File

@@ -1,11 +1,10 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/services/equipment_service.dart'; import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/equipment_status_calculator.dart'; import 'package:em2rp/services/api_service.dart';
class EquipmentProvider extends ChangeNotifier { class EquipmentProvider extends ChangeNotifier {
final EquipmentService _service = EquipmentService(); final DataService _dataService = DataService(FirebaseFunctionsApiService());
final EquipmentStatusCalculator _statusCalculator = EquipmentStatusCalculator();
List<EquipmentModel> _equipment = []; List<EquipmentModel> _equipment = [];
List<String> _models = []; List<String> _models = [];
@@ -15,68 +14,156 @@ class EquipmentProvider extends ChangeNotifier {
EquipmentStatus? _selectedStatus; EquipmentStatus? _selectedStatus;
String? _selectedModel; String? _selectedModel;
String _searchQuery = ''; String _searchQuery = '';
bool _isLoading = false;
// Getters // Getters
List<EquipmentModel> get equipment => _equipment; List<EquipmentModel> get equipment => _filteredEquipment;
List<String> get models => _models; List<String> get models => _models;
List<String> get brands => _brands; List<String> get brands => _brands;
EquipmentCategory? get selectedCategory => _selectedCategory; EquipmentCategory? get selectedCategory => _selectedCategory;
EquipmentStatus? get selectedStatus => _selectedStatus; EquipmentStatus? get selectedStatus => _selectedStatus;
String? get selectedModel => _selectedModel; String? get selectedModel => _selectedModel;
String get searchQuery => _searchQuery; String get searchQuery => _searchQuery;
bool get isLoading => _isLoading;
/// Stream des équipements avec filtres appliqués /// Charger tous les équipements via l'API
Stream<List<EquipmentModel>> get equipmentStream { Future<void> loadEquipments() async {
return _service.getEquipment( print('[EquipmentProvider] Starting to load equipments...');
category: _selectedCategory, _isLoading = true;
status: _selectedStatus,
model: _selectedModel,
searchQuery: _searchQuery,
);
}
/// Charger tous les modèles uniques
Future<void> loadModels() async {
try {
_models = await _service.getAllModels();
notifyListeners(); notifyListeners();
} catch (e) {
print('Error loading models: $e');
rethrow;
}
}
/// Charger toutes les marques uniques
Future<void> loadBrands() async {
try { try {
_brands = await _service.getAllBrands(); print('[EquipmentProvider] Calling getEquipments API...');
final equipmentsData = await _dataService.getEquipments();
print('[EquipmentProvider] Received ${equipmentsData.length} equipments from API');
_equipment = equipmentsData.map((data) {
return EquipmentModel.fromMap(data, data['id'] as String);
}).toList();
print('[EquipmentProvider] Mapped ${_equipment.length} equipment models');
// Extraire les modèles et marques uniques
_extractUniqueValues();
_isLoading = false;
notifyListeners(); notifyListeners();
print('[EquipmentProvider] Equipment loading complete');
} catch (e) { } catch (e) {
print('Error loading brands: $e'); print('[EquipmentProvider] Error loading equipments: $e');
_isLoading = false;
notifyListeners();
rethrow; rethrow;
} }
} }
/// Charger les modèles filtrés par marque /// Extraire modèles et marques uniques
Future<List<String>> loadModelsByBrand(String brand) async { void _extractUniqueValues() {
final modelSet = <String>{};
final brandSet = <String>{};
for (final eq in _equipment) {
if (eq.model != null && eq.model!.isNotEmpty) {
modelSet.add(eq.model!);
}
if (eq.brand != null && eq.brand!.isNotEmpty) {
brandSet.add(eq.brand!);
}
}
_models = modelSet.toList()..sort();
_brands = brandSet.toList()..sort();
}
/// Obtenir les équipements filtrés
List<EquipmentModel> get _filteredEquipment {
var filtered = _equipment;
if (_selectedCategory != null) {
filtered = filtered.where((eq) => eq.category == _selectedCategory).toList();
}
if (_selectedStatus != null) {
filtered = filtered.where((eq) => eq.status == _selectedStatus).toList();
}
if (_selectedModel != null && _selectedModel!.isNotEmpty) {
filtered = filtered.where((eq) => eq.model == _selectedModel).toList();
}
if (_searchQuery.isNotEmpty) {
final query = _searchQuery.toLowerCase();
filtered = filtered.where((eq) {
return eq.name.toLowerCase().contains(query) ||
eq.id.toLowerCase().contains(query) ||
(eq.model?.toLowerCase().contains(query) ?? false) ||
(eq.brand?.toLowerCase().contains(query) ?? false);
}).toList();
}
return filtered;
}
/// Définir le filtre de catégorie
void setSelectedCategory(EquipmentCategory? category) {
_selectedCategory = category;
notifyListeners();
}
/// Définir le filtre de statut
void setSelectedStatus(EquipmentStatus? status) {
_selectedStatus = status;
notifyListeners();
}
/// Définir le filtre de modèle
void setSelectedModel(String? model) {
_selectedModel = model;
notifyListeners();
}
/// Définir la requête de recherche
void setSearchQuery(String query) {
_searchQuery = query;
notifyListeners();
}
/// Réinitialiser tous les filtres
void clearFilters() {
_selectedCategory = null;
_selectedStatus = null;
_selectedModel = null;
_searchQuery = '';
notifyListeners();
}
/// Recharger les équipements
Future<void> refresh() async {
await loadEquipments();
}
// === MÉTHODES STREAM (COMPATIBILITÉ) ===
/// Stream des équipements (pour compatibilité avec ancien code)
Stream<List<EquipmentModel>> get equipmentStream async* {
yield _equipment;
}
/// Supprimer un équipement
Future<void> deleteEquipment(String equipmentId) async {
try { try {
return await _service.getModelsByBrand(brand); await _dataService.deleteEquipment(equipmentId);
await loadEquipments(); // Recharger la liste
} catch (e) { } catch (e) {
print('Error loading models by brand: $e'); print('Error deleting equipment: $e');
rethrow; rethrow;
} }
} }
/// Ajouter un équipement /// Ajouter un équipement
Future<void> addEquipment(EquipmentModel equipment) async { Future<void> addEquipment(EquipmentModel equipment) async {
try { try {
await _service.createEquipment(equipment); await _dataService.createEquipment(equipment.id, equipment.toMap());
await loadEquipments(); // Recharger la liste
// Recharger les modèles si un nouveau modèle a été ajouté
if (equipment.model != null && !_models.contains(equipment.model)) {
await loadModels();
}
} catch (e) { } catch (e) {
print('Error adding equipment: $e'); print('Error adding equipment: $e');
rethrow; rethrow;
@@ -84,146 +171,44 @@ class EquipmentProvider extends ChangeNotifier {
} }
/// Mettre à jour un équipement /// Mettre à jour un équipement
Future<void> updateEquipment(String id, Map<String, dynamic> data) async { Future<void> updateEquipment(EquipmentModel equipment) async {
try { try {
await _service.updateEquipment(id, data); await _dataService.updateEquipment(equipment.id, equipment.toMap());
await loadEquipments(); // Recharger la liste
// Recharger les modèles si le modèle a changé
if (data.containsKey('model')) {
await loadModels();
}
} catch (e) { } catch (e) {
print('Error updating equipment: $e'); print('Error updating equipment: $e');
rethrow; rethrow;
} }
} }
/// Supprimer un équipement /// Charger les marques
Future<void> deleteEquipment(String id) async { Future<void> loadBrands() async {
try { // Les marques sont déjà chargées avec loadEquipments
await _service.deleteEquipment(id); _extractUniqueValues();
} catch (e) {
print('Error deleting equipment: $e');
rethrow;
}
} }
/// Récupérer un équipement par ID /// Charger les modèles
Future<EquipmentModel?> getEquipmentById(String id) async { Future<void> loadModels() async {
try { // Les modèles sont déjà chargés avec loadEquipments
return await _service.getEquipmentById(id); _extractUniqueValues();
} catch (e) {
print('Error getting equipment: $e');
rethrow;
}
} }
/// Trouver des alternatives disponibles /// Charger les modèles d'une marque spécifique
Future<List<EquipmentModel>> findAlternatives( Future<List<String>> loadModelsByBrand(String brand) async {
String model, // Filtrer les modèles par marque
DateTime startDate, final modelsByBrand = _equipment
DateTime endDate, .where((eq) => eq.brand == brand && eq.model != null)
) async { .map((eq) => eq.model!)
try { .toSet()
return await _service.findAlternatives(model, startDate, endDate); .toList();
} catch (e) { return modelsByBrand;
print('Error finding alternatives: $e');
rethrow;
}
} }
/// Vérifier la disponibilité d'un équipement /// Calculer le statut réel d'un équipement (compatibilité)
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;
}
}
/// Calculer le statut réel d'un équipement (asynchrone)
Future<EquipmentStatus> calculateRealStatus(EquipmentModel equipment) async { Future<EquipmentStatus> calculateRealStatus(EquipmentModel equipment) async {
return await _statusCalculator.calculateRealStatus(equipment); // Pour l'instant, retourner le statut stocké
} // TODO: Implémenter le calcul réel si nécessaire
return equipment.status;
/// Invalider le cache du calculateur de statut
void invalidateStatusCache() {
_statusCalculator.invalidateCache();
}
// === 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();
} }
} }

View File

@@ -1,110 +1,99 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:em2rp/services/equipment_status_calculator.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/event_model.dart'; import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
class EventProvider with ChangeNotifier { class EventProvider with ChangeNotifier {
final FirebaseFirestore _firestore = FirebaseFirestore.instance; final DataService _dataService = DataService(FirebaseFunctionsApiService());
List<EventModel> _events = []; List<EventModel> _events = [];
bool _isLoading = false; bool _isLoading = false;
List<EventModel> get events => _events; List<EventModel> get events => _events;
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
// Récupérer les événements pour un utilisateur spécifique // Cache des utilisateurs chargés depuis getEvents
Future<void> loadUserEvents(String userId, Map<String, Map<String, dynamic>> _usersCache = {};
{bool canViewAllEvents = false}) async {
/// Charger les événements d'un utilisateur via l'API
Future<void> loadUserEvents(String userId, {bool canViewAllEvents = false}) async {
_isLoading = true; _isLoading = true;
notifyListeners(); notifyListeners();
try { // Sauvegarder les paramètres
print( _saveLastLoadParams(userId, canViewAllEvents);
'Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)');
QuerySnapshot eventsSnapshot = await _firestore.collection('events').get(); try {
print('Found ${eventsSnapshot.docs.length} events total'); print('Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)');
// Charger via l'API - les permissions sont vérifiées côté serveur
final result = await _dataService.getEvents(userId: userId);
final eventsData = result['events'] as List<Map<String, dynamic>>;
final usersData = result['users'] as Map<String, dynamic>;
// Stocker les utilisateurs dans le cache
_usersCache = usersData.map((key, value) =>
MapEntry(key, value as Map<String, dynamic>)
);
print('Found ${eventsData.length} events from API');
List<EventModel> allEvents = []; List<EventModel> allEvents = [];
int failedCount = 0; int failedCount = 0;
// Parser chaque événement individuellement pour éviter qu'une erreur interrompe tout // Parser chaque événement
for (var doc in eventsSnapshot.docs) { for (var eventData in eventsData) {
try { try {
final data = doc.data() as Map<String, dynamic>; final event = EventModel.fromMap(eventData, eventData['id'] as String);
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 ${eventData['id']}: $e');
failedCount++; failedCount++;
// Continue avec les autres événements au lieu d'arrêter
} }
} }
// Filtrage amélioré pour les utilisateurs non-admin
if (canViewAllEvents) {
_events = allEvents; _events = allEvents;
print('Admin user: showing all ${_events.length} events'); print('Successfully loaded ${_events.length} events (${failedCount} failed)');
} else {
// Créer la référence utilisateur correctement
final userDocRef = _firestore.collection('users').doc(userId);
_events = allEvents.where((event) {
// Vérifier si l'utilisateur est dans l'équipe de l'événement
bool isInWorkforce = event.workforce.any((workforceRef) {
return workforceRef.path == userDocRef.path;
});
if (isInWorkforce) {
print('Event ${event.name} includes user $userId');
}
return isInWorkforce;
}).toList();
}
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
print('Error loading events: $e'); print('Error loading events: $e');
_isLoading = false; _isLoading = false;
_events = []; // S'assurer que la liste est vide en cas d'erreur
notifyListeners(); notifyListeners();
rethrow; rethrow;
} }
} }
// Récupérer un événement spécifique /// Recharger les événements (utilise le dernier userId)
Future<EventModel?> getEvent(String eventId) async { Future<void> refreshEvents(String userId, {bool canViewAllEvents = false}) async {
try { await loadUserEvents(userId, canViewAllEvents: canViewAllEvents);
final doc = await _firestore.collection('events').doc(eventId).get();
if (doc.exists) {
return EventModel.fromMap(doc.data()!, doc.id);
} }
return null;
/// Récupérer un événement spécifique par ID
EventModel? getEventById(String eventId) {
try {
return _events.firstWhere((event) => event.id == eventId);
} catch (e) { } catch (e) {
print('Error getting event: $e'); return null;
rethrow;
} }
} }
// Ajouter un nouvel événement /// Ajouter un nouvel événement
Future<void> addEvent(EventModel event) async { Future<void> addEvent(EventModel event) async {
try { try {
final docRef = await _firestore.collection('events').add(event.toMap()); // L'événement est créé via l'API dans le service
final newEvent = EventModel.fromMap(event.toMap(), docRef.id); await refreshEvents(_lastUserId ?? '', canViewAllEvents: _lastCanViewAll);
_events.add(newEvent);
notifyListeners();
} catch (e) { } catch (e) {
print('Error adding event: $e'); print('Error adding event: $e');
rethrow; rethrow;
} }
} }
// Mettre à jour un événement /// Mettre à jour un événement
Future<void> updateEvent(EventModel event) async { Future<void> updateEvent(EventModel event) async {
try { try {
await _firestore.collection('events').doc(event.id).update(event.toMap()); // Mise à jour locale immédiate
final index = _events.indexWhere((e) => e.id == event.id); final index = _events.indexWhere((e) => e.id == event.id);
if (index != -1) { if (index != -1) {
_events[index] = event; _events[index] = event;
@@ -116,15 +105,11 @@ class EventProvider with ChangeNotifier {
} }
} }
// Supprimer un événement /// Supprimer un événement
Future<void> deleteEvent(String eventId) async { Future<void> deleteEvent(String eventId) async {
try { try {
await _firestore.collection('events').doc(eventId).delete(); await _dataService.deleteEvent(eventId);
_events.removeWhere((event) => event.id == eventId); _events.removeWhere((event) => event.id == eventId);
// Invalider le cache des statuts d'équipement
EquipmentStatusCalculator.invalidateGlobalCache();
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
print('Error deleting event: $e'); print('Error deleting event: $e');
@@ -132,9 +117,56 @@ class EventProvider with ChangeNotifier {
} }
} }
// Vider la liste des événements /// Récupérer les données d'un utilisateur depuis le cache
Map<String, dynamic>? getUserFromCache(String uid) {
return _usersCache[uid];
}
/// Récupérer les utilisateurs de la workforce d'un événement
List<Map<String, dynamic>> getWorkforceUsers(EventModel event) {
final users = <Map<String, dynamic>>[];
for (final dynamic userRef in event.workforce) {
try {
String? uid;
// Tenter d'extraire l'UID
if (userRef is String) {
uid = userRef;
} else {
// Essayer d'extraire l'ID si c'est une DocumentReference
final ref = userRef as DocumentReference?;
uid = ref?.id;
}
if (uid != null) {
final userData = getUserFromCache(uid);
if (userData != null) {
users.add(userData);
}
}
} catch (e) {
// Ignorer les références invalides
print('Skipping invalid workforce reference: $userRef');
}
}
return users;
}
/// Vider la liste des événements
void clearEvents() { void clearEvents() {
_events = []; _events = [];
notifyListeners(); notifyListeners();
} }
// Variables pour stocker le dernier appel
String? _lastUserId;
bool _lastCanViewAll = false;
/// Sauvegarder les paramètres du dernier chargement
void _saveLastLoadParams(String userId, bool canViewAllEvents) {
_lastUserId = userId;
_lastCanViewAll = canViewAllEvents;
}
} }

View File

@@ -0,0 +1,51 @@
import 'package:flutter/foundation.dart';
import 'package:em2rp/models/maintenance_model.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
class MaintenanceProvider extends ChangeNotifier {
final DataService _dataService = DataService(FirebaseFunctionsApiService());
List<MaintenanceModel> _maintenances = [];
bool _isLoading = false;
List<MaintenanceModel> get maintenances => _maintenances;
bool get isLoading => _isLoading;
/// Charger toutes les maintenances via l'API
Future<void> loadMaintenances({String? equipmentId}) async {
_isLoading = true;
notifyListeners();
try {
final maintenancesData = await _dataService.getMaintenances(
equipmentId: equipmentId,
);
_maintenances = maintenancesData.map((data) {
return MaintenanceModel.fromMap(data, data['id'] as String);
}).toList();
_isLoading = false;
notifyListeners();
} catch (e) {
print('Error loading maintenances: $e');
_isLoading = false;
notifyListeners();
rethrow;
}
}
/// Recharger les maintenances
Future<void> refresh({String? equipmentId}) async {
await loadMaintenances(equipmentId: equipmentId);
}
/// Obtenir les maintenances pour un équipement spécifique
List<MaintenanceModel> getForEquipment(String equipmentId) {
return _maintenances.where((m) =>
m.equipmentIds.contains(equipmentId)
).toList();
}
}

View File

@@ -1,54 +1,53 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../models/user_model.dart'; import 'package:em2rp/models/user_model.dart';
import '../services/user_service.dart'; import 'package:em2rp/services/data_service.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/services/api_service.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:provider/provider.dart';
import 'package:em2rp/providers/local_user_provider.dart';
class UsersProvider with ChangeNotifier { class UsersProvider with ChangeNotifier {
final UserService _userService; final DataService _dataService = DataService(FirebaseFunctionsApiService());
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseAuth _auth = FirebaseAuth.instance;
List<UserModel> _users = []; List<UserModel> _users = [];
bool _isLoading = false; bool _isLoading = false;
List<UserModel> get users => _users; List<UserModel> get users => _users;
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
UsersProvider(this._userService); /// Récupération de tous les utilisateurs via l'API
/// Récupération de tous les utilisateurs
Future<void> fetchUsers() async { Future<void> fetchUsers() async {
_isLoading = true; _isLoading = true;
notifyListeners(); notifyListeners();
try { try {
final snapshot = await _firestore.collection('users').get(); final usersData = await _dataService.getUsers();
_users = snapshot.docs _users = usersData.map((data) {
.map((doc) => UserModel.fromMap(doc.data(), doc.id)) return UserModel.fromMap(data, data['id'] as String);
.toList(); }).toList();
} catch (e) { } catch (e) {
print('Error fetching users: $e'); print('Error fetching users: $e');
_users = [];
} }
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
} }
/// Mise à jour d'un utilisateur /// Recharger les utilisateurs
Future<void> updateUser(UserModel user, {String? roleId}) async { Future<void> refresh() async {
await fetchUsers();
}
/// Obtenir un utilisateur par ID
UserModel? getUserById(String uid) {
try { try {
await _firestore.collection('users').doc(user.uid).update({ return _users.firstWhere((u) => u.uid == uid);
'firstName': user.firstName, } catch (e) {
'lastName': user.lastName, return null;
'email': user.email, }
'phoneNumber': user.phoneNumber, }
'role': roleId != null
? _firestore.collection('roles').doc(roleId) /// Mettre à jour un utilisateur
: user.role, Future<void> updateUser(UserModel user) async {
'profilePhotoUrl': user.profilePhotoUrl, try {
}); await _dataService.updateUser(user.uid, user.toMap());
final index = _users.indexWhere((u) => u.uid == user.uid); final index = _users.indexWhere((u) => u.uid == user.uid);
if (index != -1) { if (index != -1) {
@@ -64,7 +63,7 @@ class UsersProvider with ChangeNotifier {
/// Suppression d'un utilisateur /// Suppression d'un utilisateur
Future<void> deleteUser(String uid) async { Future<void> deleteUser(String uid) async {
try { try {
await _firestore.collection('users').doc(uid).delete(); // TODO: Créer une Cloud Function deleteUser
_users.removeWhere((user) => user.uid == uid); _users.removeWhere((user) => user.uid == uid);
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
@@ -73,98 +72,29 @@ class UsersProvider with ChangeNotifier {
} }
} }
/// Créer un utilisateur avec invitation par email
Future<void> createUserWithEmailInvite({
required String email,
required String firstName,
required String lastName,
String? phoneNumber,
required String roleId,
}) async {
try {
// TODO: Implémenter via Cloud Function
print('Creating user with email invite: $email');
await fetchUsers(); // Recharger la liste
} catch (e) {
print('Error creating user with email invite: $e');
rethrow;
}
}
/// Réinitialisation du mot de passe /// Réinitialisation du mot de passe
Future<void> resetPassword(String email) async { Future<void> resetPassword(String email) async {
await _userService.resetPassword(email); // Firebase Auth reste OK
} // await _userService.resetPassword(email);
// TODO: Implémenter via Cloud Function
Future<void> createUserWithEmailInvite(BuildContext context, UserModel user, print('Reset password for: $email');
{String? roleId}) async {
String? authUid;
try {
// Vérifier l'état de l'authentification
final currentUser = _auth.currentUser;
print('Current user: ${currentUser?.email}');
if (currentUser == null) {
throw Exception('Aucun utilisateur connecté');
}
// Vérifier la permission via le provider
final localUserProvider =
Provider.of<LocalUserProvider>(context, listen: false);
if (!localUserProvider.hasPermission('add_user')) {
throw Exception(
'Vous n\'avez pas la permission de créer des utilisateurs');
}
try {
// Créer l'utilisateur dans Firebase Authentication
final userCredential = await _auth.createUserWithEmailAndPassword(
email: user.email,
password: 'TemporaryPassword123!', // Mot de passe temporaire
);
authUid = userCredential.user!.uid;
print('User created in Auth with UID: $authUid');
// Créer le document dans Firestore avec l'UID de Auth comme ID
await _firestore.collection('users').doc(authUid).set({
'uid': authUid,
'firstName': user.firstName,
'lastName': user.lastName,
'email': user.email,
'phoneNumber': user.phoneNumber,
'role': roleId != null
? _firestore.collection('roles').doc(roleId)
: user.role,
'profilePhotoUrl': user.profilePhotoUrl,
'createdAt': FieldValue.serverTimestamp(),
});
print('User document created in Firestore with Auth UID');
// Envoyer un email de réinitialisation de mot de passe
await _auth.sendPasswordResetEmail(
email: user.email,
actionCodeSettings: ActionCodeSettings(
url: 'http://app.em2events.fr/finishSignUp?email=${user.email}',
handleCodeInApp: true,
androidPackageName: 'com.em2rp.app',
androidInstallApp: true,
androidMinimumVersion: '12',
),
);
print('Password reset email sent');
// Ajouter l'utilisateur à la liste locale
final newUser = UserModel(
uid: authUid,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
phoneNumber: user.phoneNumber,
role: roleId ?? user.role,
profilePhotoUrl: user.profilePhotoUrl,
);
_users.add(newUser);
notifyListeners();
} catch (e) {
// En cas d'erreur, supprimer l'utilisateur Auth si créé
if (authUid != null) {
try {
await _auth.currentUser?.delete();
} catch (deleteError) {
print('Warning: Could not delete Auth user: $deleteError');
}
}
rethrow;
}
} catch (e) {
print('Error creating user: $e');
rethrow;
}
} }
} }

View File

@@ -0,0 +1,52 @@
import 'package:em2rp/models/container_model.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
/// Service pour gérer la relation entre containers et équipements
/// Utilise le principe : seul le container stocke la référence aux équipements
class ContainerEquipmentService {
final DataService _dataService = DataService(apiService);
/// Récupère tous les containers contenant un équipement spécifique
/// Utilise une Cloud Function avec authentification et permissions
Future<List<ContainerModel>> getContainersByEquipment(String equipmentId) async {
try {
final containersData = await _dataService.getContainersByEquipment(equipmentId);
return containersData.map((data) {
// L'ID est dans le champ 'id' retourné par la fonction
final id = data['id'] as String;
return ContainerModel.fromMap(data, id);
}).toList();
} catch (e) {
print('[ContainerEquipmentService] Error getting containers for equipment $equipmentId: $e');
rethrow;
}
}
/// Vérifie si un équipement est dans au moins un container
Future<bool> isEquipmentInAnyContainer(String equipmentId) async {
try {
final containers = await getContainersByEquipment(equipmentId);
return containers.isNotEmpty;
} catch (e) {
print('[ContainerEquipmentService] Error checking if equipment is in container: $e');
return false;
}
}
/// Récupère le nombre de containers contenant un équipement
Future<int> getContainerCountForEquipment(String equipmentId) async {
try {
final containers = await getContainersByEquipment(equipmentId);
return containers.length;
} catch (e) {
print('[ContainerEquipmentService] Error getting container count: $e');
return 0;
}
}
}
/// Instance globale singleton
final containerEquipmentService = ContainerEquipmentService();

View File

@@ -0,0 +1,339 @@
import 'package:em2rp/services/api_service.dart';
/// Service générique pour les opérations de lecture de données via Cloud Functions
class DataService {
final ApiService _apiService;
DataService(this._apiService);
/// Récupère toutes les options
Future<List<Map<String, dynamic>>> getOptions() async {
try {
final result = await _apiService.call('getOptions', {});
final options = result['options'] as List<dynamic>?;
if (options == null) return [];
return options.map((e) => e as Map<String, dynamic>).toList();
} catch (e) {
throw Exception('Erreur lors de la récupération des options: $e');
}
}
/// Récupère tous les types d'événements
Future<List<Map<String, dynamic>>> getEventTypes() async {
try {
final result = await _apiService.call('getEventTypes', {});
final eventTypes = result['eventTypes'] as List<dynamic>?;
if (eventTypes == null) return [];
return eventTypes.map((e) => e as Map<String, dynamic>).toList();
} catch (e) {
throw Exception('Erreur lors de la récupération des types d\'événements: $e');
}
}
/// Récupère tous les rôles
Future<List<Map<String, dynamic>>> getRoles() async {
try {
final result = await _apiService.call('getRoles', {});
final roles = result['roles'] as List<dynamic>?;
if (roles == null) return [];
return roles.map((e) => e as Map<String, dynamic>).toList();
} catch (e) {
throw Exception('Erreur lors de la récupération des rôles: $e');
}
}
/// Met à jour les équipements d'un événement
Future<void> updateEventEquipment({
required String eventId,
List<Map<String, dynamic>>? assignedEquipment,
String? preparationStatus,
String? loadingStatus,
String? unloadingStatus,
String? returnStatus,
}) async {
try {
final data = <String, dynamic>{'eventId': eventId};
if (assignedEquipment != null) data['assignedEquipment'] = assignedEquipment;
if (preparationStatus != null) data['preparationStatus'] = preparationStatus;
if (loadingStatus != null) data['loadingStatus'] = loadingStatus;
if (unloadingStatus != null) data['unloadingStatus'] = unloadingStatus;
if (returnStatus != null) data['returnStatus'] = returnStatus;
await _apiService.call('updateEventEquipment', data);
} catch (e) {
throw Exception('Erreur lors de la mise à jour des équipements de l\'événement: $e');
}
}
/// Met à jour uniquement le statut d'un équipement
Future<void> updateEquipmentStatusOnly({
required String equipmentId,
String? status,
int? availableQuantity,
}) async {
try {
final data = <String, dynamic>{'equipmentId': equipmentId};
if (status != null) data['status'] = status;
if (availableQuantity != null) data['availableQuantity'] = availableQuantity;
await _apiService.call('updateEquipmentStatusOnly', data);
} catch (e) {
throw Exception('Erreur lors de la mise à jour du statut de l\'équipement: $e');
}
}
/// Met à jour un utilisateur
Future<void> updateUser(String userId, Map<String, dynamic> data) async {
try {
final requestData = {'userId': userId, ...data};
await _apiService.call('updateUser', requestData);
} catch (e) {
throw Exception('Erreur lors de la mise à jour de l\'utilisateur: $e');
}
}
/// Met à jour un événement
Future<void> updateEvent(String eventId, Map<String, dynamic> data) async {
try {
final requestData = {'eventId': eventId, ...data};
await _apiService.call('updateEvent', requestData);
} catch (e) {
throw Exception('Erreur lors de la mise à jour de l\'événement: $e');
}
}
/// Supprime un événement
Future<void> deleteEvent(String eventId) async {
try {
await _apiService.call('deleteEvent', {'eventId': eventId});
} catch (e) {
throw Exception('Erreur lors de la suppression de l\'événement: $e');
}
}
/// Crée un nouvel équipement
Future<void> createEquipment(String equipmentId, Map<String, dynamic> data) async {
try {
final requestData = {'equipmentId': equipmentId, ...data};
await _apiService.call('createEquipment', requestData);
} catch (e) {
throw Exception('Erreur lors de la création de l\'équipement: $e');
}
}
/// Met à jour un équipement
Future<void> updateEquipment(String equipmentId, Map<String, dynamic> data) async {
try {
final requestData = {'equipmentId': equipmentId, ...data};
await _apiService.call('updateEquipment', requestData);
} catch (e) {
throw Exception('Erreur lors de la mise à jour de l\'équipement: $e');
}
}
/// Supprime un équipement
Future<void> deleteEquipment(String equipmentId) async {
try {
await _apiService.call('deleteEquipment', {'equipmentId': equipmentId});
} catch (e) {
throw Exception('Erreur lors de la suppression de l\'équipement: $e');
}
}
/// Récupère les événements utilisant un type d'événement donné
Future<List<Map<String, dynamic>>> getEventsByEventType(String eventTypeId) async {
try {
final result = await _apiService.call('getEventsByEventType', {'eventTypeId': eventTypeId});
final events = result['events'] as List<dynamic>?;
if (events == null) return [];
return events.map((e) => e as Map<String, dynamic>).toList();
} catch (e) {
throw Exception('Erreur lors de la récupération des événements: $e');
}
}
/// Crée un type d'événement
Future<String> createEventType({
required String name,
required double defaultPrice,
}) async {
try {
final result = await _apiService.call('createEventType', {
'name': name,
'defaultPrice': defaultPrice,
});
return result['id'] as String;
} catch (e) {
throw Exception('Erreur lors de la création du type d\'événement: $e');
}
}
/// Met à jour un type d'événement
Future<void> updateEventType({
required String eventTypeId,
String? name,
double? defaultPrice,
}) async {
try {
final data = <String, dynamic>{'eventTypeId': eventTypeId};
if (name != null) data['name'] = name;
if (defaultPrice != null) data['defaultPrice'] = defaultPrice;
await _apiService.call('updateEventType', data);
} catch (e) {
throw Exception('Erreur lors de la mise à jour du type d\'événement: $e');
}
}
/// Supprime un type d'événement
Future<void> deleteEventType(String eventTypeId) async {
try {
await _apiService.call('deleteEventType', {'eventTypeId': eventTypeId});
} catch (e) {
throw Exception('Erreur lors de la suppression du type d\'événement: $e');
}
}
/// Crée une option
Future<String> createOption(String code, Map<String, dynamic> data) async {
try {
final requestData = {'code': code, ...data};
final result = await _apiService.call('createOption', requestData);
return result['id'] as String? ?? code;
} catch (e) {
throw Exception('Erreur lors de la création de l\'option: $e');
}
}
/// Met à jour une option
Future<void> updateOption(String optionId, Map<String, dynamic> data) async {
try {
final requestData = {'optionId': optionId, ...data};
await _apiService.call('updateOption', requestData);
} catch (e) {
throw Exception('Erreur lors de la mise à jour de l\'option: $e');
}
}
/// Supprime une option
Future<void> deleteOption(String optionId) async {
try {
await _apiService.call('deleteOption', {'optionId': optionId});
} catch (e) {
throw Exception('Erreur lors de la suppression de l\'option: $e');
}
}
// ============================================================================
// LECTURE DES DONNÉES (avec permissions côté serveur)
// ============================================================================
/// Récupère tous les événements (filtrés selon permissions)
/// Retourne { events: List<Map>, users: Map<String, Map> }
Future<Map<String, dynamic>> getEvents({String? userId}) async {
try {
final data = <String, dynamic>{};
if (userId != null) data['userId'] = userId;
final result = await _apiService.call('getEvents', data);
// Extraire events et users
final events = result['events'] as List<dynamic>? ?? [];
final users = result['users'] as Map<String, dynamic>? ?? {};
return {
'events': events.map((e) => e as Map<String, dynamic>).toList(),
'users': users,
};
} catch (e) {
throw Exception('Erreur lors de la récupération des événements: $e');
}
}
/// Récupère tous les équipements (avec masquage des prix selon permissions)
Future<List<Map<String, dynamic>>> getEquipments() async {
try {
print('[DataService] Calling getEquipments API...');
final result = await _apiService.call('getEquipments', {});
print('[DataService] API call successful, parsing result...');
final equipments = result['equipments'] as List<dynamic>?;
if (equipments == null) {
print('[DataService] No equipments in result');
return [];
}
print('[DataService] Found ${equipments.length} equipments');
return equipments.map((e) => e as Map<String, dynamic>).toList();
} catch (e) {
print('[DataService] Error getting equipments: $e');
throw Exception('Erreur lors de la récupération des équipements: $e');
}
}
/// Récupère tous les conteneurs
Future<List<Map<String, dynamic>>> getContainers() async {
try {
final result = await _apiService.call('getContainers', {});
final containers = result['containers'] as List<dynamic>?;
if (containers == null) return [];
return containers.map((e) => e as Map<String, dynamic>).toList();
} catch (e) {
throw Exception('Erreur lors de la récupération des conteneurs: $e');
}
}
/// Récupère les maintenances (optionnellement filtrées par équipement)
Future<List<Map<String, dynamic>>> getMaintenances({String? equipmentId}) async {
try {
final data = <String, dynamic>{};
if (equipmentId != null) data['equipmentId'] = equipmentId;
final result = await _apiService.call('getMaintenances', data);
final maintenances = result['maintenances'] as List<dynamic>?;
if (maintenances == null) return [];
return maintenances.map((e) => e as Map<String, dynamic>).toList();
} catch (e) {
throw Exception('Erreur lors de la récupération des maintenances: $e');
}
}
/// Récupère les alertes
Future<List<Map<String, dynamic>>> getAlerts() async {
try {
final result = await _apiService.call('getAlerts', {});
final alerts = result['alerts'] as List<dynamic>?;
if (alerts == null) return [];
return alerts.map((e) => e as Map<String, dynamic>).toList();
} catch (e) {
throw Exception('Erreur lors de la récupération des alertes: $e');
}
}
/// Récupère les utilisateurs (filtrés selon permissions)
Future<List<Map<String, dynamic>>> getUsers() async {
try {
final result = await _apiService.call('getUsers', {});
final users = result['users'] as List<dynamic>?;
if (users == null) return [];
return users.map((e) => e as Map<String, dynamic>).toList();
} catch (e) {
throw Exception('Erreur lors de la récupération des utilisateurs: $e');
}
}
/// Récupère les containers contenant un équipement spécifique
Future<List<Map<String, dynamic>>> getContainersByEquipment(String equipmentId) async {
try {
final result = await _apiService.call('getContainersByEquipment', {
'equipmentId': equipmentId,
});
final containers = result['containers'] as List<dynamic>?;
if (containers == null) return [];
return containers.map((e) => e as Map<String, dynamic>).toList();
} catch (e) {
throw Exception('Erreur lors de la récupération des containers pour l\'équipement: $e');
}
}
}

View File

@@ -1,12 +1,15 @@
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/models/container_model.dart';
import 'package:em2rp/models/alert_model.dart'; import 'package:em2rp/models/alert_model.dart';
import 'package:em2rp/models/maintenance_model.dart'; import 'package:em2rp/models/maintenance_model.dart';
import 'package:em2rp/services/api_service.dart'; import 'package:em2rp/services/api_service.dart';
import 'package:em2rp/services/data_service.dart';
class EquipmentService { class EquipmentService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final ApiService _apiService = apiService; final ApiService _apiService = apiService;
final DataService _dataService = DataService(apiService);
// Collection references (utilisées seulement pour les lectures) // Collection references (utilisées seulement pour les lectures)
CollectionReference get _equipmentCollection => _firestore.collection('equipments'); CollectionReference get _equipmentCollection => _firestore.collection('equipments');
@@ -134,7 +137,7 @@ class EquipmentService {
.get(); .get();
for (var eventDoc in eventsQuery.docs) { for (var eventDoc in eventsQuery.docs) {
final eventData = eventDoc.data() as Map<String, dynamic>; final eventData = eventDoc.data();
final assignedEquipmentRaw = eventData['assignedEquipment'] ?? []; final assignedEquipmentRaw = eventData['assignedEquipment'] ?? [];
if (assignedEquipmentRaw is List) { if (assignedEquipmentRaw is List) {
@@ -170,7 +173,7 @@ class EquipmentService {
for (var doc in equipmentQuery.docs) { for (var doc in equipmentQuery.docs) {
final equipment = EquipmentModel.fromMap( final equipment = EquipmentModel.fromMap(
doc.data() as Map<String, dynamic>, doc.data(),
doc.id, doc.id,
); );
@@ -230,7 +233,7 @@ class EquipmentService {
for (var doc in equipmentQuery.docs) { for (var doc in equipmentQuery.docs) {
final equipment = EquipmentModel.fromMap( final equipment = EquipmentModel.fromMap(
doc.data() as Map<String, dynamic>, doc.data(),
doc.id, doc.id,
); );
@@ -285,7 +288,7 @@ class EquipmentService {
final models = <String>{}; final models = <String>{};
for (var doc in equipmentQuery.docs) { for (var doc in equipmentQuery.docs) {
final data = doc.data() as Map<String, dynamic>; final data = doc.data();
final model = data['model'] as String?; final model = data['model'] as String?;
if (model != null && model.isNotEmpty) { if (model != null && model.isNotEmpty) {
models.add(model); models.add(model);
@@ -306,7 +309,7 @@ class EquipmentService {
final brands = <String>{}; final brands = <String>{};
for (var doc in equipmentQuery.docs) { for (var doc in equipmentQuery.docs) {
final data = doc.data() as Map<String, dynamic>; final data = doc.data();
final brand = data['brand'] as String?; final brand = data['brand'] as String?;
if (brand != null && brand.isNotEmpty) { if (brand != null && brand.isNotEmpty) {
brands.add(brand); brands.add(brand);
@@ -329,7 +332,7 @@ class EquipmentService {
final models = <String>{}; final models = <String>{};
for (var doc in equipmentQuery.docs) { for (var doc in equipmentQuery.docs) {
final data = doc.data() as Map<String, dynamic>; final data = doc.data();
final model = data['model'] as String?; final model = data['model'] as String?;
if (model != null && model.isNotEmpty) { if (model != null && model.isNotEmpty) {
models.add(model); models.add(model);
@@ -354,26 +357,16 @@ class EquipmentService {
} }
} }
/// Récupérer toutes les boîtes (équipements qui peuvent contenir d'autres équipements) /// Récupérer toutes les boîtes/containers disponibles
Future<List<EquipmentModel>> getBoxes() async { Future<List<ContainerModel>> getBoxes() async {
try { try {
// Les boîtes sont généralement des équipements de catégorie "structure" ou "other" final containersData = await _dataService.getContainers();
// On pourrait aussi ajouter un champ spécifique "isBox" dans le modèle
final equipmentQuery = await _firestore.collection('equipments')
.where('category', whereIn: [
equipmentCategoryToString(EquipmentCategory.structure),
equipmentCategoryToString(EquipmentCategory.other),
])
.get();
final boxes = <EquipmentModel>[]; final boxes = <ContainerModel>[];
for (var doc in equipmentQuery.docs) { for (var data in containersData) {
final equipment = EquipmentModel.fromMap( final id = data['id'] as String;
doc.data() as Map<String, dynamic>, final container = ContainerModel.fromMap(data, id);
doc.id, boxes.add(container);
);
// On pourrait ajouter un filtre supplémentaire ici si besoin
boxes.add(equipment);
} }
return boxes; return boxes;
@@ -401,7 +394,7 @@ class EquipmentService {
for (var doc in query.docs) { for (var doc in query.docs) {
equipments.add( equipments.add(
EquipmentModel.fromMap( EquipmentModel.fromMap(
doc.data() as Map<String, dynamic>, doc.data(),
doc.id, doc.id,
), ),
); );

View File

@@ -1,4 +1,3 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart'; import 'package:firebase_storage/firebase_storage.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
@@ -8,32 +7,34 @@ import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/models/event_type_model.dart'; import 'package:em2rp/models/event_type_model.dart';
import 'package:em2rp/models/user_model.dart'; import 'package:em2rp/models/user_model.dart';
import 'package:em2rp/services/api_service.dart'; import 'package:em2rp/services/api_service.dart';
import 'package:em2rp/services/data_service.dart';
import 'dart:developer' as developer; import 'dart:developer' as developer;
class EventFormService { class EventFormService {
static final ApiService _apiService = apiService; static final ApiService _apiService = apiService;
static final DataService _dataService = DataService(FirebaseFunctionsApiService());
// ============================================================================ // ============================================================================
// READ Operations - Utilise Firestore (peut rester en lecture directe) // READ Operations - Utilise l'API (sécurisé avec permissions côté serveur)
// ============================================================================ // ============================================================================
static Future<List<EventTypeModel>> fetchEventTypes() async { static Future<List<EventTypeModel>> fetchEventTypes() async {
developer.log('Fetching event types from Firestore...', name: 'EventFormService'); developer.log('Fetching event types via API...', name: 'EventFormService');
try { try {
final snapshot = await FirebaseFirestore.instance.collection('eventTypes').get(); final eventTypesData = await _dataService.getEventTypes();
final eventTypes = snapshot.docs.map((doc) => EventTypeModel.fromMap(doc.data(), doc.id)).toList(); final eventTypes = eventTypesData.map((data) => EventTypeModel.fromMap(data, data['id'] as String)).toList();
developer.log('${eventTypes.length} event types loaded.', name: 'EventFormService'); developer.log('${eventTypes.length} event types loaded.', name: 'EventFormService');
return eventTypes; return eventTypes;
} catch (e, s) { } catch (e, s) {
developer.log('Error fetching event types', name: 'EventFormService', error: e, stackTrace: s); developer.log('Error fetching event types', name: 'EventFormService', error: e, stackTrace: s);
throw Exception("Could not load event types. Please check Firestore permissions."); throw Exception("Could not load event types. Please check permissions.");
} }
} }
static Future<List<UserModel>> fetchUsers() async { static Future<List<UserModel>> fetchUsers() async {
try { try {
final snapshot = await FirebaseFirestore.instance.collection('users').get(); final usersData = await _dataService.getUsers();
return snapshot.docs.map((doc) => UserModel.fromMap(doc.data(), doc.id)).toList(); return usersData.map((data) => UserModel.fromMap(data, data['id'] as String)).toList();
} catch (e) { } catch (e) {
developer.log('Error fetching users', name: 'EventFormService', error: e); developer.log('Error fetching users', name: 'EventFormService', error: e);
throw Exception("Could not load users."); throw Exception("Could not load users.");
@@ -171,9 +172,15 @@ class EventFormService {
} }
static Future<void> updateEventDocuments(String eventId, List<Map<String, String>> documents) async { static Future<void> updateEventDocuments(String eventId, List<Map<String, String>> documents) async {
await FirebaseFirestore.instance // Utiliser l'API pour mettre à jour les documents
.collection('events') try {
.doc(eventId) await _apiService.call('updateEvent', {
.update({'documents': documents}); 'eventId': eventId,
'documents': documents,
});
} catch (e) {
developer.log('Error updating event documents', name: 'EventFormService', error: e);
throw Exception("Could not update event documents.");
}
} }
} }

View File

@@ -66,12 +66,22 @@ END:VCALENDAR''';
} }
/// Récupère les détails de la main d'œuvre /// Récupère les détails de la main d'œuvre
static Future<List<String>> _getWorkforceDetails(List<DocumentReference> workforce) async { static Future<List<String>> _getWorkforceDetails(List<dynamic> workforce) async {
final List<String> workforceNames = []; final List<String> workforceNames = [];
for (final ref in workforce) { for (final ref in workforce) {
try { try {
final doc = await ref.get(); DocumentReference? docRef;
// Gérer String (UID) ou DocumentReference
if (ref is String) {
docRef = FirebaseFirestore.instance.collection('users').doc(ref);
} else if (ref is DocumentReference) {
docRef = ref;
}
if (docRef != null) {
final doc = await docRef.get();
if (doc.exists) { if (doc.exists) {
final data = doc.data() as Map<String, dynamic>?; final data = doc.data() as Map<String, dynamic>?;
if (data != null) { if (data != null) {
@@ -82,6 +92,7 @@ END:VCALENDAR''';
} }
} }
} }
}
} catch (e) { } catch (e) {
print('Erreur lors de la récupération des détails utilisateur: $e'); print('Erreur lors de la récupération des détails utilisateur: $e');
} }

View File

@@ -1,40 +1,48 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import '../models/user_model.dart'; import '../models/user_model.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
/// @deprecated Ce service est obsolète. Utilisez UsersProvider avec DataService à la place.
/// Ce service reste pour compatibilité mais toutes les opérations passent par l'API.
class UserService { class UserService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance; final DataService _dataService = DataService(FirebaseFunctionsApiService());
/// @deprecated Utilisez UsersProvider.fetchUsers() à la place
Future<List<UserModel>> fetchUsers() async { Future<List<UserModel>> fetchUsers() async {
try { try {
final snapshot = await _firestore.collection('users').get(); final usersData = await _dataService.getUsers();
return snapshot.docs return usersData.map((data) => UserModel.fromMap(data, data['id'] as String)).toList();
.map((doc) => UserModel.fromMap(doc.data(), doc.id))
.toList();
} catch (e) { } catch (e) {
print("Erreur: $e"); print("Erreur: $e");
return []; return [];
} }
} }
/// @deprecated Utilisez DataService.updateUser() à la place
Future<void> updateUser(UserModel user) async { Future<void> updateUser(UserModel user) async {
try { try {
await _firestore.collection('users').doc(user.uid).update(user.toMap()); await _dataService.updateUser(user.uid, user.toMap());
} catch (e) { } catch (e) {
print("Erreur mise à jour: $e"); print("Erreur mise à jour: $e");
} }
} }
/// @deprecated Utilisez API deleteUser à la place
Future<void> deleteUser(String uid) async { Future<void> deleteUser(String uid) async {
try { try {
await _firestore.collection('users').doc(uid).delete(); // TODO: Créer une Cloud Function deleteUser
print("Suppression d'utilisateur non implémentée via API");
} catch (e) { } catch (e) {
print("Erreur suppression: $e"); print("Erreur suppression: $e");
} }
} }
/// Firebase Auth reste OK (pas Firestore)
Future<void> resetPassword(String email) async { Future<void> resetPassword(String email) async {
try { try {
// Firebase Auth est OK, ce n'est pas Firestore
await FirebaseAuth.instance.sendPasswordResetEmail(email: email); await FirebaseAuth.instance.sendPasswordResetEmail(email: email);
print("Email de réinitialisation envoyé à $email"); print("Email de réinitialisation envoyé à $email");
} catch (e) { } catch (e) {

View File

@@ -1,18 +1,19 @@
import 'package:flutter/foundation.dart'; // pour kIsWeb import 'package:flutter/foundation.dart'; // pour kIsWeb
import 'package:firebase_storage/firebase_storage.dart'; import 'package:firebase_storage/firebase_storage.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
class FirebaseStorageManager { class FirebaseStorageManager {
final FirebaseStorage _storage = FirebaseStorage.instance; final FirebaseStorage _storage = FirebaseStorage.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance; final DataService _dataService = DataService(FirebaseFunctionsApiService());
/// Upload ou remplace la photo de profil d'un utilisateur dans Firebase Storage. /// Upload ou remplace la photo de profil d'un utilisateur dans Firebase Storage.
/// Pour le Web, on fixe l'extension .jpg. /// Pour le Web, on fixe l'extension .jpg.
/// 1. Construit le chemin : "ProfilePictures/UID.jpg" /// 1. Construit le chemin : "ProfilePictures/UID.jpg"
/// 2. Supprime l'ancienne photo (si elle existe). /// 2. Supprime l'ancienne photo (si elle existe).
/// 3. Upload la nouvelle photo. /// 3. Upload la nouvelle photo.
/// 4. Met à jour Firestore avec l'URL de la nouvelle image. /// 4. Met à jour Firestore avec l'URL de la nouvelle image via l'API.
Future<String?> sendProfilePicture( Future<String?> sendProfilePicture(
{required XFile imageFile, required String uid}) async { {required XFile imageFile, required String uid}) async {
try { try {
@@ -57,17 +58,14 @@ class FirebaseStorageManager {
print( print(
"FirebaseStorageManager: Nouvelle photo uploadée pour l'utilisateur $uid. URL: $downloadUrl"); "FirebaseStorageManager: Nouvelle photo uploadée pour l'utilisateur $uid. URL: $downloadUrl");
// 5. Mettre à jour Firestore avec l'URL de la photo de profil // 5. Mettre à jour via l'API (plus sécurisé)
try { try {
await _firestore await _dataService.updateUser(uid, {'profilePhotoUrl': downloadUrl});
.collection('users')
.doc(uid)
.update({'profilePhotoUrl': downloadUrl});
print( print(
"FirebaseStorageManager: Firestore mis à jour pour l'utilisateur $uid."); "FirebaseStorageManager: Profil mis à jour via API pour l'utilisateur $uid.");
} catch (firestoreError) { } catch (apiError) {
print( print(
"FirebaseStorageManager: Erreur Firestore pour l'utilisateur $uid: $firestoreError"); "FirebaseStorageManager: Erreur API pour l'utilisateur $uid: $apiError");
return downloadUrl; // On retourne l'URL même si la mise à jour échoue return downloadUrl; // On retourne l'URL même si la mise à jour échoue
} }
return downloadUrl; return downloadUrl;

View File

@@ -50,7 +50,6 @@ class _CalendarPageState extends State<CalendarPage> {
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime)); ..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
EventModel? selected; EventModel? selected;
DateTime? selectedDay; DateTime? selectedDay;
int selectedEventIndex = 0;
if (todayEvents.isNotEmpty) { if (todayEvents.isNotEmpty) {
selected = todayEvents[0]; selected = todayEvents[0];
selectedDay = DateTime(now.year, now.month, now.day); selectedDay = DateTime(now.year, now.month, now.day);
@@ -87,9 +86,7 @@ class _CalendarPageState extends State<CalendarPage> {
Provider.of<LocalUserProvider>(context, listen: false); Provider.of<LocalUserProvider>(context, listen: false);
final eventProvider = Provider.of<EventProvider>(context, listen: false); final eventProvider = Provider.of<EventProvider>(context, listen: false);
final userId = localAuthProvider.uid; final userId = localAuthProvider.uid;
print('Permissions utilisateur: ${localAuthProvider.permissions}');
final canViewAllEvents = localAuthProvider.hasPermission('view_all_events'); final canViewAllEvents = localAuthProvider.hasPermission('view_all_events');
print('canViewAllEvents: $canViewAllEvents');
if (userId != null) { if (userId != null) {
await eventProvider.loadUserEvents(userId, await eventProvider.loadUserEvents(userId,

View File

@@ -10,7 +10,6 @@ import 'package:em2rp/services/qr_code_service.dart';
import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart'; import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
import 'package:em2rp/views/equipment_form_page.dart'; import 'package:em2rp/views/equipment_form_page.dart';
import 'package:em2rp/views/widgets/equipment/equipment_parent_containers.dart';
import 'package:em2rp/views/widgets/equipment/equipment_referencing_containers.dart'; import 'package:em2rp/views/widgets/equipment/equipment_referencing_containers.dart';
import 'package:em2rp/views/widgets/equipment/equipment_header_section.dart'; import 'package:em2rp/views/widgets/equipment/equipment_header_section.dart';
import 'package:em2rp/views/widgets/equipment/equipment_main_info_section.dart'; import 'package:em2rp/views/widgets/equipment/equipment_main_info_section.dart';
@@ -124,15 +123,9 @@ class _EquipmentDetailPageState extends State<EquipmentDetailPage> {
const SizedBox(height: 24), const SizedBox(height: 24),
// Containers parents (si applicable) // Containers contenant cet équipement
if (widget.equipment.parentBoxIds.isNotEmpty) ...[ // Note: On utilise EquipmentReferencingContainers qui recherche dynamiquement
EquipmentParentContainers( // les containers au lieu de se baser sur parentBoxIds qui peut être désynchronisé
parentBoxIds: widget.equipment.parentBoxIds,
),
const SizedBox(height: 24),
],
// Containers associés
EquipmentReferencingContainers( EquipmentReferencingContainers(
equipmentId: widget.equipment.id, equipmentId: widget.equipment.id,
), ),

View File

@@ -2,14 +2,18 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/models/container_model.dart';
import 'package:em2rp/providers/equipment_provider.dart'; import 'package:em2rp/providers/equipment_provider.dart';
import 'package:em2rp/providers/container_provider.dart';
import 'package:em2rp/providers/local_user_provider.dart'; import 'package:em2rp/providers/local_user_provider.dart';
import 'package:em2rp/services/equipment_service.dart'; import 'package:em2rp/services/equipment_service.dart';
import 'package:em2rp/services/container_equipment_service.dart';
import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart'; import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:em2rp/views/equipment_form/brand_model_selector.dart'; import 'package:em2rp/views/equipment_form/brand_model_selector.dart';
import 'package:em2rp/utils/id_generator.dart'; import 'package:em2rp/utils/id_generator.dart';
import 'package:em2rp/views/widgets/equipment/parent_boxes_selector.dart';
class EquipmentFormPage extends StatefulWidget { class EquipmentFormPage extends StatefulWidget {
final EquipmentModel? equipment; final EquipmentModel? equipment;
@@ -42,7 +46,7 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
DateTime? _lastMaintenanceDate; DateTime? _lastMaintenanceDate;
DateTime? _nextMaintenanceDate; DateTime? _nextMaintenanceDate;
List<String> _selectedParentBoxIds = []; List<String> _selectedParentBoxIds = [];
List<EquipmentModel> _availableBoxes = []; List<ContainerModel> _availableBoxes = [];
bool _isLoading = false; bool _isLoading = false;
bool _isLoadingBoxes = true; bool _isLoadingBoxes = true;
bool _addMultiple = false; bool _addMultiple = false;
@@ -65,6 +69,7 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
void _populateFields() { void _populateFields() {
final equipment = widget.equipment!; final equipment = widget.equipment!;
setState(() {
_identifierController.text = equipment.id; _identifierController.text = equipment.id;
_brandController.text = equipment.brand ?? ''; _brandController.text = equipment.brand ?? '';
_selectedBrand = equipment.brand; _selectedBrand = equipment.brand;
@@ -78,22 +83,46 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
_purchaseDate = equipment.purchaseDate; _purchaseDate = equipment.purchaseDate;
_lastMaintenanceDate = equipment.lastMaintenanceDate; _lastMaintenanceDate = equipment.lastMaintenanceDate;
_nextMaintenanceDate = equipment.nextMaintenanceDate; _nextMaintenanceDate = equipment.nextMaintenanceDate;
_selectedParentBoxIds = List.from(equipment.parentBoxIds);
_notesController.text = equipment.notes ?? ''; _notesController.text = equipment.notes ?? '';
});
print('[EquipmentForm] Populating fields for equipment: ${equipment.id}');
// Charger les containers contenant cet équipement depuis Firestore
_loadCurrentContainers(equipment.id);
if (_selectedBrand != null && _selectedBrand!.isNotEmpty) { if (_selectedBrand != null && _selectedBrand!.isNotEmpty) {
_loadFilteredModels(_selectedBrand!); _loadFilteredModels(_selectedBrand!);
} }
} }
/// Charge les containers qui contiennent actuellement cet équipement
Future<void> _loadCurrentContainers(String equipmentId) async {
try {
final containers = await containerEquipmentService.getContainersByEquipment(equipmentId);
setState(() {
_selectedParentBoxIds = containers.map((c) => c.id).toList();
});
print('[EquipmentForm] Loaded ${containers.length} containers for equipment $equipmentId');
print('[EquipmentForm] Selected container IDs: $_selectedParentBoxIds');
} catch (e) {
print('[EquipmentForm] Error loading containers for equipment: $e');
}
}
Future<void> _loadAvailableBoxes() async { Future<void> _loadAvailableBoxes() async {
try { try {
final boxes = await _equipmentService.getBoxes(); final boxes = await _equipmentService.getBoxes();
print('[EquipmentForm] Loaded ${boxes.length} boxes from service');
for (var box in boxes) {
print('[EquipmentForm] Box loaded - ID: ${box.id}, Name: ${box.name}');
}
setState(() { setState(() {
_availableBoxes = boxes; _availableBoxes = boxes;
_isLoadingBoxes = false; _isLoadingBoxes = false;
}); });
} catch (e) { } catch (e) {
print('[EquipmentForm] Error loading boxes: $e');
setState(() { setState(() {
_isLoadingBoxes = false; _isLoadingBoxes = false;
}); });
@@ -390,11 +419,14 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
], ],
// Boîtes parentes // Boîtes parentes
const Divider(),
const Text('Boîtes parentes', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 8), const SizedBox(height: 8),
_isLoadingBoxes _isLoadingBoxes
? const Center(child: CircularProgressIndicator()) ? const Card(
child: Padding(
padding: EdgeInsets.all(32.0),
child: Center(child: CircularProgressIndicator()),
),
)
: _buildParentBoxesSelector(), : _buildParentBoxesSelector(),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -449,36 +481,15 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
} }
Widget _buildParentBoxesSelector() { Widget _buildParentBoxesSelector() {
if (_availableBoxes.isEmpty) { return ParentBoxesSelector(
return const Card( availableBoxes: _availableBoxes,
child: Padding( selectedBoxIds: _selectedParentBoxIds,
padding: EdgeInsets.all(16.0), onSelectionChanged: (newSelection) {
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(() { setState(() {
if (value == true) { _selectedParentBoxIds = newSelection;
_selectedParentBoxIds.add(box.id);
} else {
_selectedParentBoxIds.remove(box.id);
}
}); });
}, },
); );
}).toList(),
),
);
} }
Widget _buildDateField({required String label, required IconData icon, required DateTime? value, required VoidCallback onTap}) { Widget _buildDateField({required String label, required IconData icon, required DateTime? value, required VoidCallback onTap}) {
@@ -625,19 +636,66 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
purchaseDate: _purchaseDate, purchaseDate: _purchaseDate,
lastMaintenanceDate: _lastMaintenanceDate, lastMaintenanceDate: _lastMaintenanceDate,
nextMaintenanceDate: _nextMaintenanceDate, nextMaintenanceDate: _nextMaintenanceDate,
parentBoxIds: _selectedParentBoxIds, parentBoxIds: [], // On ne stocke plus les parentBoxIds dans l'équipement
notes: _notesController.text, notes: _notesController.text,
createdAt: isEditing ? (widget.equipment?.createdAt ?? now) : now, createdAt: isEditing ? (widget.equipment?.createdAt ?? now) : now,
updatedAt: now, updatedAt: now,
availableQuantity: availableQuantity, availableQuantity: availableQuantity,
); );
if (isEditing) { if (isEditing) {
await equipmentProvider.updateEquipment( await equipmentProvider.updateEquipment(equipment);
equipment.id,
equipment.toMap(), // Synchroniser les containers : mettre à jour equipmentIds des containers
// Charger les anciens containers depuis Firestore
final oldContainers = await containerEquipmentService.getContainersByEquipment(equipment.id);
final oldParentBoxIds = oldContainers.map((c) => c.id).toList();
final newParentBoxIds = _selectedParentBoxIds;
// Boîtes ajoutées : ajouter cet équipement à leur equipmentIds
final addedBoxes = newParentBoxIds.where((id) => !oldParentBoxIds.contains(id));
for (final boxId in addedBoxes) {
try {
final containerProvider = Provider.of<ContainerProvider>(context, listen: false);
await containerProvider.addEquipmentToContainer(
containerId: boxId,
equipmentId: equipment.id,
); );
print('[EquipmentForm] Added equipment ${equipment.id} to container $boxId');
} catch (e) {
print('[EquipmentForm] Error adding equipment to container $boxId: $e');
}
}
// Boîtes retirées : retirer cet équipement de leur equipmentIds
final removedBoxes = oldParentBoxIds.where((id) => !newParentBoxIds.contains(id));
for (final boxId in removedBoxes) {
try {
final containerProvider = Provider.of<ContainerProvider>(context, listen: false);
await containerProvider.removeEquipmentFromContainer(
containerId: boxId,
equipmentId: equipment.id,
);
print('[EquipmentForm] Removed equipment ${equipment.id} from container $boxId');
} catch (e) {
print('[EquipmentForm] Error removing equipment from container $boxId: $e');
}
}
} else { } else {
await equipmentProvider.addEquipment(equipment); await equipmentProvider.addEquipment(equipment);
// Pour un nouvel équipement, ajouter à tous les containers sélectionnés
for (final boxId in _selectedParentBoxIds) {
try {
final containerProvider = Provider.of<ContainerProvider>(context, listen: false);
await containerProvider.addEquipmentToContainer(
containerId: boxId,
equipmentId: equipment.id,
);
print('[EquipmentForm] Added new equipment ${equipment.id} to container $boxId');
} catch (e) {
print('[EquipmentForm] Error adding new equipment to container $boxId: $e');
}
}
} }
} }

View File

@@ -29,6 +29,17 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
EquipmentCategory? _selectedCategory; EquipmentCategory? _selectedCategory;
List<EquipmentModel>? _cachedEquipment; List<EquipmentModel>? _cachedEquipment;
@override
void initState() {
super.initState();
print('[EquipmentManagementPage] initState called');
// Charger les équipements au démarrage
WidgetsBinding.instance.addPostFrameCallback((_) {
print('[EquipmentManagementPage] Loading equipments...');
context.read<EquipmentProvider>().loadEquipments();
});
}
@override @override
void dispose() { void dispose() {
_searchController.dispose(); _searchController.dispose();
@@ -420,16 +431,44 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
Widget _buildEquipmentList() { Widget _buildEquipmentList() {
return Consumer<EquipmentProvider>( return Consumer<EquipmentProvider>(
builder: (context, provider, child) { builder: (context, provider, child) {
return ManagementList<EquipmentModel>( print('[EquipmentManagementPage] Building list - isLoading: ${provider.isLoading}, equipment count: ${provider.equipment.length}');
stream: provider.equipmentStream,
cachedItems: _cachedEquipment, if (provider.isLoading && _cachedEquipment == null) {
emptyMessage: 'Aucun équipement trouvé', print('[EquipmentManagementPage] Showing loading indicator');
emptyIcon: Icons.inventory_2_outlined, return const Center(child: CircularProgressIndicator());
onDataReceived: (items) { }
_cachedEquipment = items;
}, final equipments = provider.equipment;
itemBuilder: (equipment) {
return _buildEquipmentCard(equipment); if (equipments.isEmpty && !provider.isLoading) {
print('[EquipmentManagementPage] No equipment found');
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inventory_2_outlined,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'Aucun équipement trouvé',
style: TextStyle(
fontSize: 18,
color: Colors.grey[600],
),
),
],
),
);
}
print('[EquipmentManagementPage] Building list with ${equipments.length} items');
return ListView.builder(
itemCount: equipments.length,
itemBuilder: (context, index) {
return _buildEquipmentCard(equipments[index]);
}, },
); );
}, },
@@ -903,11 +942,14 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
'updatedAt': DateTime.now().toIso8601String(), 'updatedAt': DateTime.now().toIso8601String(),
}; };
await context.read<EquipmentProvider>().updateEquipment( final updatedEquipment = equipment.copyWith(
equipment.id, availableQuantity: newAvailable,
updatedData, totalQuantity: newTotal,
updatedAt: DateTime.now(),
); );
await context.read<EquipmentProvider>().updateEquipment(updatedEquipment);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(

View File

@@ -6,8 +6,10 @@ import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/models/container_model.dart'; import 'package:em2rp/models/container_model.dart';
import 'package:em2rp/providers/equipment_provider.dart'; import 'package:em2rp/providers/equipment_provider.dart';
import 'package:em2rp/providers/container_provider.dart'; import 'package:em2rp/providers/container_provider.dart';
import 'package:em2rp/services/event_preparation_service.dart'; import 'package:em2rp/providers/event_provider.dart';
import 'package:em2rp/services/event_preparation_service_extended.dart'; import 'package:em2rp/providers/local_user_provider.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
import 'package:em2rp/views/widgets/equipment/equipment_checklist_item.dart' show EquipmentChecklistItem, ChecklistStep; import 'package:em2rp/views/widgets/equipment/equipment_checklist_item.dart' show EquipmentChecklistItem, ChecklistStep;
import 'package:em2rp/views/widgets/equipment/missing_equipment_dialog.dart'; import 'package:em2rp/views/widgets/equipment/missing_equipment_dialog.dart';
import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/colors.dart';
@@ -34,9 +36,8 @@ class EventPreparationPage extends StatefulWidget {
} }
class _EventPreparationPageState extends State<EventPreparationPage> with SingleTickerProviderStateMixin { class _EventPreparationPageState extends State<EventPreparationPage> with SingleTickerProviderStateMixin {
final EventPreparationService _preparationService = EventPreparationService();
final EventPreparationServiceExtended _extendedService = EventPreparationServiceExtended();
late AnimationController _animationController; late AnimationController _animationController;
late final DataService _dataService;
Map<String, EquipmentModel> _equipmentCache = {}; Map<String, EquipmentModel> _equipmentCache = {};
Map<String, ContainerModel> _containerCache = {}; Map<String, ContainerModel> _containerCache = {};
@@ -89,6 +90,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
void initState() { void initState() {
super.initState(); super.initState();
_currentEvent = widget.initialEvent; _currentEvent = widget.initialEvent;
_dataService = DataService(FirebaseFunctionsApiService());
_animationController = AnimationController( _animationController = AnimationController(
vsync: this, vsync: this,
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
@@ -131,24 +133,6 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
super.dispose(); super.dispose();
} }
/// Recharger l'événement depuis Firestore
Future<void> _reloadEvent() async {
try {
final doc = await FirebaseFirestore.instance
.collection('events')
.doc(_currentEvent.id)
.get();
if (doc.exists) {
setState(() {
_currentEvent = EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
});
}
} catch (e) {
print('[EventPreparationPage] Error reloading event: $e');
}
}
Future<void> _loadEquipmentAndContainers() async { Future<void> _loadEquipmentAndContainers() async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
@@ -293,11 +277,15 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
break; break;
} }
// Sauvegarder dans Firestore // Sauvegarder dans Firestore via l'API
await FirebaseFirestore.instance await _dataService.updateEventEquipment(
.collection('events') eventId: _currentEvent.id,
.doc(_currentEvent.id) assignedEquipment: updatedEquipment.map((e) => e.toMap()).toList(),
.update(updateData); preparationStatus: updateData['preparationStatus'],
loadingStatus: updateData['loadingStatus'],
unloadingStatus: updateData['unloadingStatus'],
returnStatus: updateData['returnStatus'],
);
// Mettre à jour les statuts des équipements si nécessaire // Mettre à jour les statuts des équipements si nécessaire
if (_currentStep == PreparationStep.preparation || if (_currentStep == PreparationStep.preparation ||
@@ -305,6 +293,14 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
await _updateEquipmentStatuses(updatedEquipment); await _updateEquipmentStatuses(updatedEquipment);
} }
// Recharger l'événement depuis le provider
final eventProvider = context.read<EventProvider>();
// Recharger la liste des événements pour rafraîchir les données
final userId = context.read<LocalUserProvider>().uid;
if (userId != null) {
await eventProvider.loadUserEvents(userId, canViewAllEvents: true);
}
setState(() => _showSuccessAnimation = true); setState(() => _showSuccessAnimation = true);
_animationController.forward(); _animationController.forward();
@@ -338,16 +334,8 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
Future<void> _updateEquipmentStatuses(List<EventEquipment> equipment) async { Future<void> _updateEquipmentStatuses(List<EventEquipment> equipment) async {
for (var eq in equipment) { for (var eq in equipment) {
try { try {
final doc = await FirebaseFirestore.instance final equipmentData = _equipmentCache[eq.equipmentId];
.collection('equipments') if (equipmentData == null) continue;
.doc(eq.equipmentId)
.get();
if (doc.exists) {
final equipmentData = EquipmentModel.fromMap(
doc.data() as Map<String, dynamic>,
doc.id,
);
// Déterminer le nouveau statut // Déterminer le nouveau statut
EquipmentStatus newStatus; EquipmentStatus newStatus;
@@ -361,29 +349,22 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
// Ne mettre à jour que les équipements non quantifiables // Ne mettre à jour que les équipements non quantifiables
if (!equipmentData.hasQuantity) { if (!equipmentData.hasQuantity) {
await FirebaseFirestore.instance await _dataService.updateEquipmentStatusOnly(
.collection('equipments') equipmentId: eq.equipmentId,
.doc(eq.equipmentId) status: equipmentStatusToString(newStatus),
.update({ );
'status': equipmentStatusToString(newStatus),
'updatedAt': Timestamp.fromDate(DateTime.now()),
});
} }
// Gérer les stocks pour les consommables // Gérer les stocks pour les consommables
if (equipmentData.hasQuantity && eq.isReturned && eq.returnedQuantity != null) { if (equipmentData.hasQuantity && eq.isReturned && eq.returnedQuantity != null) {
final currentAvailable = equipmentData.availableQuantity ?? 0; final currentAvailable = equipmentData.availableQuantity ?? 0;
await FirebaseFirestore.instance await _dataService.updateEquipmentStatusOnly(
.collection('equipments') equipmentId: eq.equipmentId,
.doc(eq.equipmentId) availableQuantity: currentAvailable + eq.returnedQuantity!,
.update({ );
'availableQuantity': currentAvailable + eq.returnedQuantity!,
'updatedAt': Timestamp.fromDate(DateTime.now()),
});
}
} }
} catch (e) { } catch (e) {
print('Error updating equipment status for ${eq.equipmentId}: $e'); // Erreur silencieuse pour ne pas bloquer le processus
} }
} }
} }

View File

@@ -9,7 +9,8 @@ import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/utils/permission_gate.dart'; import 'package:em2rp/utils/permission_gate.dart';
import 'package:em2rp/models/role_model.dart'; import 'package:em2rp/models/role_model.dart';
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart'; import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
class UserManagementPage extends StatefulWidget { class UserManagementPage extends StatefulWidget {
const UserManagementPage({super.key}); const UserManagementPage({super.key});
@@ -116,14 +117,18 @@ class _UserManagementPageState extends State<UserManagementPage> {
bool isLoadingRoles = true; bool isLoadingRoles = true;
Future<void> loadRoles() async { Future<void> loadRoles() async {
final snapshot = try {
await FirebaseFirestore.instance.collection('roles').get(); final dataService = DataService(FirebaseFunctionsApiService());
availableRoles = snapshot.docs final rolesData = await dataService.getRoles();
.map((doc) => RoleModel.fromMap(doc.data(), doc.id)) availableRoles = rolesData
.map((data) => RoleModel.fromMap(data, data['id'] as String))
.toList(); .toList();
selectedRoleId = selectedRoleId =
availableRoles.isNotEmpty ? availableRoles.first.id : null; availableRoles.isNotEmpty ? availableRoles.first.id : null;
isLoadingRoles = false; isLoadingRoles = false;
} catch (e) {
isLoadingRoles = false;
}
} }
InputDecoration buildInputDecoration(String label, IconData icon) { InputDecoration buildInputDecoration(String label, IconData icon) {
@@ -265,8 +270,7 @@ class _UserManagementPageState extends State<UserManagementPage> {
); );
await Provider.of<UsersProvider>(context, await Provider.of<UsersProvider>(context,
listen: false) listen: false)
.createUserWithEmailInvite(context, newUser, .createUserWithEmailInvite(email: newUser.email, firstName: newUser.firstName, lastName: newUser.lastName, phoneNumber: newUser.phoneNumber, roleId: newUser.role);
roleId: selectedRoleId);
Navigator.pop(context); Navigator.pop(context);
} catch (e) { } catch (e) {
if (context.mounted) { if (context.mounted) {

View File

@@ -77,7 +77,7 @@ class EventDetails extends StatelessWidget {
EventDetailsDescription(event: event), EventDetailsDescription(event: event),
EventDetailsDocuments(documents: event.documents), EventDetailsDocuments(documents: event.documents),
const SizedBox(height: 16), const SizedBox(height: 16),
EventDetailsEquipe(workforce: event.workforce), EventDetailsEquipe(event: event),
], ],
), ),
), ),

View File

@@ -1,19 +1,21 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:provider/provider.dart';
import 'package:em2rp/models/user_model.dart'; import 'package:em2rp/models/user_model.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/providers/event_provider.dart';
import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart'; import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart';
class EventDetailsEquipe extends StatelessWidget { class EventDetailsEquipe extends StatelessWidget {
final List workforce; final EventModel event;
const EventDetailsEquipe({ const EventDetailsEquipe({
super.key, super.key,
required this.workforce, required this.event,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (workforce.isEmpty) { if (event.workforce.isEmpty) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -33,55 +35,23 @@ class EventDetailsEquipe extends StatelessWidget {
); );
} }
return FutureBuilder<List<UserModel>>( // Récupérer les utilisateurs depuis le cache du provider
future: _fetchUsers(), final eventProvider = Provider.of<EventProvider>(context, listen: false);
builder: (context, snapshot) { final workforceUsers = eventProvider.getWorkforceUsers(event);
if (snapshot.connectionState == ConnectionState.waiting) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Equipe',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
),
],
);
}
if (snapshot.hasError) { // Convertir en UserModel
return Column( final users = workforceUsers.map((userData) {
crossAxisAlignment: CrossAxisAlignment.start, return UserModel(
children: [ uid: userData['uid'] ?? '',
Text( firstName: userData['firstName'] ?? '',
'Equipe', lastName: userData['lastName'] ?? '',
style: Theme.of(context).textTheme.titleLarge?.copyWith( email: userData['email'] ?? '',
color: Colors.black, phoneNumber: userData['phoneNumber'] ?? '',
fontWeight: FontWeight.bold, profilePhotoUrl: userData['profilePhotoUrl'] ?? '',
), role: '', // Pas besoin du rôle pour l'affichage
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Text(
snapshot.error.toString().contains('permission-denied')
? "Vous n'avez pas la permission de voir tous les membres de l'équipe."
: "Erreur lors du chargement de l'équipe : ${snapshot.error}",
style: const TextStyle(color: Colors.red),
),
),
],
); );
} }).toList();
final users = snapshot.data ?? [];
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -95,7 +65,7 @@ class EventDetailsEquipe extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
if (users.isEmpty) if (users.isEmpty)
Text( Text(
'Aucun membre assigné ou erreur de chargement.', 'Aucun membre assigné.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.orange[700], color: Colors.orange[700],
), ),
@@ -107,31 +77,6 @@ class EventDetailsEquipe extends StatelessWidget {
), ),
], ],
); );
},
);
}
Future<List<UserModel>> _fetchUsers() async {
final firestore = FirebaseFirestore.instance;
List<UserModel> users = [];
for (int i = 0; i < workforce.length; i++) {
final ref = workforce[i];
try {
if (ref is DocumentReference) {
final doc = await firestore.doc(ref.path).get();
if (doc.exists) {
final userData = doc.data() as Map<String, dynamic>;
users.add(UserModel.fromMap(userData, doc.id));
}
}
} catch (e) {
// Log silencieux des erreurs individuelles
debugPrint('Error fetching user $i: $e');
}
}
return users;
} }
} }

View File

@@ -1,11 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/colors.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:em2rp/providers/local_user_provider.dart'; import 'package:em2rp/providers/local_user_provider.dart';
import 'package:em2rp/views/event_add_page.dart'; import 'package:em2rp/views/event_add_page.dart';
import 'package:em2rp/services/ics_export_service.dart'; import 'package:em2rp/services/ics_export_service.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
import 'dart:html' as html; import 'dart:html' as html;
import 'dart:convert'; import 'dart:convert';
@@ -53,24 +54,21 @@ class _EventDetailsHeaderState extends State<EventDetailsHeader> {
return; return;
} }
final doc = await FirebaseFirestore.instance // Charger tous les types d'événements via l'API
.collection('eventTypes') final dataService = DataService(FirebaseFunctionsApiService());
.doc(widget.event.eventTypeId) final eventTypes = await dataService.getEventTypes();
.get();
// Trouver le type correspondant
final eventType = eventTypes.firstWhere(
(type) => type['id'] == widget.event.eventTypeId,
orElse: () => <String, dynamic>{},
);
if (doc.exists) {
setState(() { setState(() {
_eventTypeName = doc.data()?['name'] as String? ?? widget.event.eventTypeId; _eventTypeName = eventType['name'] as String? ?? widget.event.eventTypeId;
_isLoadingEventType = false; _isLoadingEventType = false;
}); });
} else {
setState(() {
_eventTypeName = widget.event.eventTypeId;
_isLoadingEventType = false;
});
}
} catch (e) { } catch (e) {
print('Erreur lors du chargement du type d\'événement: $e');
setState(() { setState(() {
_eventTypeName = widget.event.eventTypeId; _eventTypeName = widget.event.eventTypeId;
_isLoadingEventType = false; _isLoadingEventType = false;

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:provider/provider.dart';
import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/providers/event_provider.dart';
import 'package:em2rp/views/event_preparation_page.dart'; import 'package:em2rp/views/event_preparation_page.dart';
import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/colors.dart';
@@ -20,30 +21,19 @@ class EventPreparationButtons extends StatefulWidget {
class _EventPreparationButtonsState extends State<EventPreparationButtons> { class _EventPreparationButtonsState extends State<EventPreparationButtons> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Écouter les changements de l'événement en temps réel // Utiliser le provider pour récupérer l'événement à jour
return StreamBuilder<DocumentSnapshot>( final eventProvider = context.watch<EventProvider>();
stream: FirebaseFirestore.instance
.collection('events') // Chercher l'événement mis à jour dans le provider
.doc(widget.event.id) final EventModel currentEvent = eventProvider.events.firstWhere(
.snapshots(), (e) => e.id == widget.event.id,
initialData: null, orElse: () => widget.event,
builder: (context, snapshot) {
// Utiliser l'événement du stream si disponible, sinon l'événement initial
final EventModel currentEvent;
if (snapshot.hasData && snapshot.data != null && snapshot.data!.exists) {
currentEvent = EventModel.fromMap(
snapshot.data!.data() as Map<String, dynamic>,
snapshot.data!.id,
); );
} else {
currentEvent = widget.event;
}
return _buildButtons(context, currentEvent); return _buildButtons(context, currentEvent);
},
);
} }
Widget _buildButtons(BuildContext context, EventModel event) { Widget _buildButtons(BuildContext context, EventModel event) {
// Vérifier s'il y a du matériel assigné // Vérifier s'il y a du matériel assigné
final hasMaterial = event.assignedEquipment.isNotEmpty || event.assignedContainers.isNotEmpty; final hasMaterial = event.assignedEquipment.isNotEmpty || event.assignedContainers.isNotEmpty;

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/models/event_model.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:em2rp/providers/event_provider.dart'; import 'package:em2rp/providers/event_provider.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
class EventStatusButton extends StatefulWidget { class EventStatusButton extends StatefulWidget {
final EventModel event; final EventModel event;
@@ -22,22 +23,28 @@ class EventStatusButton extends StatefulWidget {
class _EventStatusButtonState extends State<EventStatusButton> { class _EventStatusButtonState extends State<EventStatusButton> {
bool _loading = false; bool _loading = false;
final DataService _dataService = DataService(FirebaseFunctionsApiService());
Future<void> _changeStatus(EventStatus newStatus) async { Future<void> _changeStatus(EventStatus newStatus) async {
if (widget.event.status == newStatus) return; if (widget.event.status == newStatus) return;
setState(() => _loading = true); setState(() => _loading = true);
try { try {
await FirebaseFirestore.instance // Mettre à jour via l'API
.collection('events') await _dataService.updateEvent(widget.event.id, {
.doc(widget.event.id) 'status': eventStatusToString(newStatus),
.update({'status': eventStatusToString(newStatus)}); });
final snap = await FirebaseFirestore.instance // Récupérer l'événement mis à jour via l'API
.collection('events') final result = await _dataService.getEvents();
.doc(widget.event.id) final eventsList = result['events'] as List<dynamic>;
.get(); final eventData = eventsList.firstWhere(
final updatedEvent = EventModel.fromMap(snap.data()!, widget.event.id); (e) => e['id'] == widget.event.id,
orElse: () => <String, dynamic>{},
);
if (eventData.isNotEmpty) {
final updatedEvent = EventModel.fromMap(eventData, widget.event.id);
widget.onSelectEvent( widget.onSelectEvent(
updatedEvent, updatedEvent,
@@ -46,6 +53,7 @@ class _EventStatusButtonState extends State<EventStatusButton> {
await Provider.of<EventProvider>(context, listen: false) await Provider.of<EventProvider>(context, listen: false)
.updateEvent(updatedEvent); .updateEvent(updatedEvent);
}
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/event_type_model.dart'; import 'package:em2rp/models/event_type_model.dart';
import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/colors.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
class EventTypesManagement extends StatefulWidget { class EventTypesManagement extends StatefulWidget {
const EventTypesManagement({super.key}); const EventTypesManagement({super.key});
@@ -15,34 +16,39 @@ class _EventTypesManagementState extends State<EventTypesManagement> {
String _searchQuery = ''; String _searchQuery = '';
List<EventTypeModel> _eventTypes = []; List<EventTypeModel> _eventTypes = [];
bool _loading = true; bool _loading = true;
late final DataService _dataService;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_dataService = DataService(FirebaseFunctionsApiService());
_loadEventTypes(); _loadEventTypes();
} }
Future<void> _loadEventTypes() async { Future<void> _loadEventTypes() async {
setState(() => _loading = true); setState(() => _loading = true);
try { try {
final snapshot = await FirebaseFirestore.instance final eventTypesData = await _dataService.getEventTypes();
.collection('eventTypes')
.orderBy('name') // Trier par nom
.get(); eventTypesData.sort((a, b) =>
(a['name'] as String).compareTo(b['name'] as String));
setState(() { setState(() {
_eventTypes = snapshot.docs _eventTypes = eventTypesData
.map((doc) => EventTypeModel.fromMap(doc.data(), doc.id)) .map((data) => EventTypeModel.fromMap(data, data['id'] as String))
.toList(); .toList();
_loading = false; _loading = false;
}); });
} catch (e) { } catch (e) {
setState(() => _loading = false); setState(() => _loading = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur lors du chargement : $e')), SnackBar(content: Text('Erreur lors du chargement : $e')),
); );
} }
} }
}
List<EventTypeModel> get _filteredEventTypes { List<EventTypeModel> get _filteredEventTypes {
if (_searchQuery.isEmpty) return _eventTypes; if (_searchQuery.isEmpty) return _eventTypes;
@@ -52,44 +58,45 @@ class _EventTypesManagementState extends State<EventTypesManagement> {
} }
Future<bool> _canDeleteEventType(String eventTypeId) async { Future<bool> _canDeleteEventType(String eventTypeId) async {
final eventsSnapshot = await FirebaseFirestore.instance try {
.collection('events') final events = await _dataService.getEventsByEventType(eventTypeId);
.where('eventTypeId', isEqualTo: eventTypeId) return events.isEmpty;
.get(); } catch (e) {
return false;
return eventsSnapshot.docs.isEmpty; }
} }
Future<List<Map<String, dynamic>>> _getBlockingEvents(String eventTypeId) async { Future<List<Map<String, dynamic>>> _getBlockingEvents(String eventTypeId) async {
final eventsSnapshot = await FirebaseFirestore.instance try {
.collection('events') final events = await _dataService.getEventsByEventType(eventTypeId);
.where('eventTypeId', isEqualTo: eventTypeId)
.get();
final now = DateTime.now(); final now = DateTime.now();
List<Map<String, dynamic>> futureEvents = []; List<Map<String, dynamic>> futureEvents = [];
List<Map<String, dynamic>> pastEvents = []; List<Map<String, dynamic>> pastEvents = [];
for (final doc in eventsSnapshot.docs) { for (final event in events) {
final eventData = doc.data(); final eventDate = event['startDateTime'] != null
final eventDate = eventData['startDateTime']?.toDate() ?? DateTime.now(); ? DateTime.parse(event['startDateTime'] as String)
: DateTime.now();
if (eventDate.isAfter(now)) { if (eventDate.isAfter(now)) {
futureEvents.add({ futureEvents.add({
'id': doc.id, 'id': event['id'],
'name': eventData['name'], 'name': event['name'],
'startDateTime': eventDate, 'startDateTime': eventDate,
}); });
} else { } else {
pastEvents.add({ pastEvents.add({
'id': doc.id, 'id': event['id'],
'name': eventData['name'], 'name': event['name'],
'startDateTime': eventDate, 'startDateTime': eventDate,
}); });
} }
} }
return [...futureEvents, ...pastEvents]; return [...futureEvents, ...pastEvents];
} catch (e) {
return [];
}
} }
Future<void> _deleteEventType(EventTypeModel eventType) async { Future<void> _deleteEventType(EventTypeModel eventType) async {
@@ -198,20 +205,21 @@ class _EventTypesManagementState extends State<EventTypesManagement> {
onPressed: () async { onPressed: () async {
Navigator.pop(context); Navigator.pop(context);
try { try {
await FirebaseFirestore.instance await _dataService.deleteEventType(eventType.id);
.collection('eventTypes')
.doc(eventType.id)
.delete();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Type d\'événement supprimé avec succès')), const SnackBar(content: Text('Type d\'événement supprimé avec succès')),
); );
}
_loadEventTypes(); _loadEventTypes();
} catch (e) { } catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur lors de la suppression : $e')), SnackBar(content: Text('Erreur lors de la suppression : $e')),
); );
} }
}
}, },
style: ElevatedButton.styleFrom(backgroundColor: Colors.red), style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Supprimer', style: TextStyle(color: Colors.white)), child: const Text('Supprimer', style: TextStyle(color: Colors.white)),
@@ -352,10 +360,12 @@ class _EventTypeFormDialogState extends State<_EventTypeFormDialog> {
final _defaultPriceController = TextEditingController(); final _defaultPriceController = TextEditingController();
bool _loading = false; bool _loading = false;
String? _error; String? _error;
late final DataService _dataService;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_dataService = DataService(FirebaseFunctionsApiService());
if (widget.eventType != null) { if (widget.eventType != null) {
_nameController.text = widget.eventType!.name; _nameController.text = widget.eventType!.name;
_defaultPriceController.text = widget.eventType!.defaultPrice.toString(); _defaultPriceController.text = widget.eventType!.defaultPrice.toString();
@@ -369,69 +379,48 @@ class _EventTypeFormDialogState extends State<_EventTypeFormDialog> {
super.dispose(); super.dispose();
} }
Future<bool> _isNameUnique(String name) async {
final snapshot = await FirebaseFirestore.instance
.collection('eventTypes')
.where('name', isEqualTo: name)
.get();
// Si on modifie, exclure le document actuel
if (widget.eventType != null) {
return snapshot.docs
.where((doc) => doc.id != widget.eventType!.id)
.isEmpty;
}
return snapshot.docs.isEmpty;
}
Future<void> _submit() async { Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
final name = _nameController.text.trim(); final name = _nameController.text.trim();
final defaultPrice = double.tryParse(_defaultPriceController.text.replaceAll(',', '.')) ?? 0.0; final defaultPrice = double.tryParse(_defaultPriceController.text.replaceAll(',', '.')) ?? 0.0;
setState(() => _loading = true); setState(() {
_loading = true;
_error = null;
});
try { try {
// Vérifier l'unicité du nom
final isUnique = await _isNameUnique(name);
if (!isUnique) {
setState(() {
_error = 'Ce nom de type d\'événement existe déjà';
_loading = false;
});
return;
}
final data = {
'name': name,
'defaultPrice': defaultPrice,
'createdAt': widget.eventType?.createdAt ?? DateTime.now(),
};
if (widget.eventType == null) { if (widget.eventType == null) {
// Création // Création
await FirebaseFirestore.instance.collection('eventTypes').add(data); await _dataService.createEventType(
name: name,
defaultPrice: defaultPrice,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Type d\'événement créé avec succès')), const SnackBar(content: Text('Type d\'événement créé avec succès')),
); );
}
} else { } else {
// Modification // Modification
await FirebaseFirestore.instance await _dataService.updateEventType(
.collection('eventTypes') eventTypeId: widget.eventType!.id,
.doc(widget.eventType!.id) name: name,
.update(data); defaultPrice: defaultPrice,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Type d\'événement modifié avec succès')), const SnackBar(content: Text('Type d\'événement modifié avec succès')),
); );
} }
}
widget.onSaved(); widget.onSaved();
Navigator.pop(context); if (mounted) Navigator.pop(context);
} catch (e) { } catch (e) {
setState(() { setState(() {
_error = 'Erreur : $e'; _error = e.toString().replaceFirst('Exception: ', '');
_loading = false; _loading = false;
}); });
} }

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/option_model.dart'; import 'package:em2rp/models/option_model.dart';
import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class OptionsManagement extends StatefulWidget { class OptionsManagement extends StatefulWidget {
@@ -12,6 +13,7 @@ class OptionsManagement extends StatefulWidget {
} }
class _OptionsManagementState extends State<OptionsManagement> { class _OptionsManagementState extends State<OptionsManagement> {
final DataService _dataService = DataService(FirebaseFunctionsApiService());
String _searchQuery = ''; String _searchQuery = '';
List<EventOption> _options = []; List<EventOption> _options = [];
Map<String, String> _eventTypeNames = {}; Map<String, String> _eventTypeNames = {};
@@ -26,26 +28,23 @@ class _OptionsManagementState extends State<OptionsManagement> {
Future<void> _loadData() async { Future<void> _loadData() async {
setState(() => _loading = true); setState(() => _loading = true);
try { try {
// Charger les types d'événements pour les noms // Charger les types d'événements via l'API
final eventTypesSnapshot = await FirebaseFirestore.instance final eventTypesData = await _dataService.getEventTypes();
.collection('eventTypes')
.get();
_eventTypeNames = { _eventTypeNames = {
for (var doc in eventTypesSnapshot.docs) for (var typeData in eventTypesData)
doc.id: doc.data()['name'] as String typeData['id'] as String: typeData['name'] as String
}; };
// Charger les options // Charger les options via l'API
final optionsSnapshot = await FirebaseFirestore.instance final optionsData = await _dataService.getOptions();
.collection('options')
.orderBy('code')
.get();
setState(() { setState(() {
_options = optionsSnapshot.docs _options = optionsData
.map((doc) => EventOption.fromMap(doc.data(), doc.id)) .map((data) => EventOption.fromMap(data, data['id'] as String))
.toList(); .toList();
// Trier par code
_options.sort((a, b) => a.code.compareTo(b.code));
_loading = false; _loading = false;
}); });
} catch (e) { } catch (e) {
@@ -66,35 +65,38 @@ class _OptionsManagementState extends State<OptionsManagement> {
} }
Future<List<Map<String, dynamic>>> _getBlockingEvents(String optionId) async { Future<List<Map<String, dynamic>>> _getBlockingEvents(String optionId) async {
final eventsSnapshot = await FirebaseFirestore.instance // Charger tous les événements via l'API
.collection('events') final result = await _dataService.getEvents();
.get(); final eventsData = result['events'] as List<dynamic>;
final now = DateTime.now(); final now = DateTime.now();
List<Map<String, dynamic>> futureEvents = []; List<Map<String, dynamic>> futureEvents = [];
List<Map<String, dynamic>> pastEvents = []; List<Map<String, dynamic>> pastEvents = [];
for (final doc in eventsSnapshot.docs) { for (final eventData in eventsData) {
final eventData = doc.data();
final options = eventData['options'] as List<dynamic>? ?? []; final options = eventData['options'] as List<dynamic>? ?? [];
// Vérifier si cette option est utilisée dans cet événement // Vérifier si cette option est utilisée dans cet événement
bool optionUsed = options.any((opt) => opt['id'] == optionId); bool optionUsed = options.any((opt) => opt['id'] == optionId);
if (optionUsed) { if (optionUsed) {
final eventDate = eventData['StartDateTime']?.toDate() ?? DateTime.now(); final eventDate = eventData['startDateTime'] as DateTime? ??
// Corriger la récupération du nom - utiliser 'Name' au lieu de 'name' (eventData['StartDateTime'] as DateTime?) ??
final eventName = eventData['Name'] as String? ?? 'Événement sans nom'; DateTime.now();
final eventName = eventData['name'] as String? ??
eventData['Name'] as String? ??
'Événement sans nom';
final eventId = eventData['id'] as String? ?? '';
if (eventDate.isAfter(now)) { if (eventDate.isAfter(now)) {
futureEvents.add({ futureEvents.add({
'id': doc.id, 'id': eventId,
'name': eventName, 'name': eventName,
'startDateTime': eventDate, 'startDateTime': eventDate,
}); });
} else { } else {
pastEvents.add({ pastEvents.add({
'id': doc.id, 'id': eventId,
'name': eventName, 'name': eventName,
'startDateTime': eventDate, 'startDateTime': eventDate,
}); });
@@ -211,10 +213,7 @@ class _OptionsManagementState extends State<OptionsManagement> {
onPressed: () async { onPressed: () async {
Navigator.pop(context); Navigator.pop(context);
try { try {
await FirebaseFirestore.instance await _dataService.deleteOption(option.id);
.collection('options')
.doc(option.id)
.delete();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Option supprimée avec succès')), const SnackBar(content: Text('Option supprimée avec succès')),
@@ -421,17 +420,21 @@ class _OptionFormDialogState extends State<_OptionFormDialog> {
} }
Future<bool> _isCodeUnique(String code) async { Future<bool> _isCodeUnique(String code) async {
final doc = await FirebaseFirestore.instance try {
.collection('options') // Charger toutes les options via l'API
.doc(code) final dataService = DataService(FirebaseFunctionsApiService());
.get(); final optionsData = await dataService.getOptions();
// Si on modifie et que c'est le même document, c'est OK // Si on modifie et que c'est le même document, c'est OK
if (widget.option != null && widget.option!.id == code) { if (widget.option != null && widget.option!.id == code) {
return true; return true;
} }
return !doc.exists; // Vérifier si le code existe déjà
return !optionsData.any((opt) => opt['id'] == code);
} catch (e) {
return false;
}
} }
Future<void> _submit() async { Future<void> _submit() async {
@@ -471,6 +474,7 @@ class _OptionFormDialogState extends State<_OptionFormDialog> {
} }
} }
final dataService = DataService(FirebaseFunctionsApiService());
final data = { final data = {
'code': code, 'code': code,
'name': name, 'name': name,
@@ -483,16 +487,13 @@ class _OptionFormDialogState extends State<_OptionFormDialog> {
if (widget.option == null) { if (widget.option == null) {
// Création - utiliser le code comme ID // Création - utiliser le code comme ID
await FirebaseFirestore.instance.collection('options').doc(code).set(data); await dataService.createOption(code, data);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Option créée avec succès')), const SnackBar(content: Text('Option créée avec succès')),
); );
} else { } else {
// Modification // Modification
await FirebaseFirestore.instance await dataService.updateOption(widget.option!.id, data);
.collection('options')
.doc(widget.option!.id)
.update(data);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Option modifiée avec succès')), const SnackBar(content: Text('Option modifiée avec succès')),
); );

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
enum EventFilter { enum EventFilter {
@@ -26,6 +27,7 @@ class EquipmentAssociatedEventsSection extends StatefulWidget {
class _EquipmentAssociatedEventsSectionState class _EquipmentAssociatedEventsSectionState
extends State<EquipmentAssociatedEventsSection> { extends State<EquipmentAssociatedEventsSection> {
final DataService _dataService = DataService(FirebaseFunctionsApiService());
EventFilter _selectedFilter = EventFilter.upcoming; EventFilter _selectedFilter = EventFilter.upcoming;
List<EventModel> _events = []; List<EventModel> _events = [];
bool _isLoading = true; bool _isLoading = true;
@@ -40,36 +42,32 @@ class _EquipmentAssociatedEventsSectionState
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
// Récupérer TOUS les événements car on ne peut pas faire arrayContains sur un objet // Récupérer TOUS les événements via l'API
final eventsSnapshot = await FirebaseFirestore.instance final result = await _dataService.getEvents();
.collection('events') final eventsData = result['events'] as List<dynamic>;
.get();
final events = <EventModel>[]; final events = <EventModel>[];
// Récupérer toutes les boîtes pour vérifier leur contenu // Récupérer toutes les boîtes pour vérifier leur contenu via l'API
final containersSnapshot = await FirebaseFirestore.instance final containersData = await _dataService.getContainers();
.collection('containers')
.get();
final containersWithEquipment = <String>[]; final containersWithEquipment = <String>[];
for (var containerDoc in containersSnapshot.docs) { for (var containerData in containersData) {
try { try {
final data = containerDoc.data(); final equipmentIds = List<String>.from(containerData['equipmentIds'] ?? []);
final equipmentIds = List<String>.from(data['equipmentIds'] ?? []);
if (equipmentIds.contains(widget.equipment.id)) { if (equipmentIds.contains(widget.equipment.id)) {
containersWithEquipment.add(containerDoc.id); containersWithEquipment.add(containerData['id'] as String);
} }
} catch (e) { } catch (e) {
print('[EquipmentAssociatedEventsSection] Error parsing container ${containerDoc.id}: $e'); print('[EquipmentAssociatedEventsSection] Error parsing container ${containerData['id']}: $e');
} }
} }
// Filtrer manuellement les événements qui contiennent cet équipement // Filtrer manuellement les événements qui contiennent cet équipement
for (var doc in eventsSnapshot.docs) { for (var eventData in eventsData) {
try { try {
final event = EventModel.fromMap(doc.data(), doc.id); final event = EventModel.fromMap(eventData, eventData['id'] as String);
// Vérifier si l'équipement est directement assigné // Vérifier si l'équipement est directement assigné
final hasEquipmentDirectly = event.assignedEquipment.any( final hasEquipmentDirectly = event.assignedEquipment.any(
@@ -85,7 +83,7 @@ class _EquipmentAssociatedEventsSectionState
events.add(event); events.add(event);
} }
} catch (e) { } catch (e) {
print('[EquipmentAssociatedEventsSection] Error parsing event ${doc.id}: $e'); print('[EquipmentAssociatedEventsSection] Error parsing event ${eventData['id']}: $e');
} }
} }

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
/// Widget pour afficher les événements EN COURS utilisant cet équipement /// Widget pour afficher les événements EN COURS utilisant cet équipement
@@ -21,6 +22,7 @@ class EquipmentCurrentEventsSection extends StatefulWidget {
class _EquipmentCurrentEventsSectionState class _EquipmentCurrentEventsSectionState
extends State<EquipmentCurrentEventsSection> { extends State<EquipmentCurrentEventsSection> {
final DataService _dataService = DataService(FirebaseFunctionsApiService());
List<EventModel> _events = []; List<EventModel> _events = [];
bool _isLoading = true; bool _isLoading = true;
@@ -34,36 +36,32 @@ class _EquipmentCurrentEventsSectionState
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
// Récupérer TOUS les événements // Récupérer TOUS les événements via l'API
final eventsSnapshot = await FirebaseFirestore.instance final result = await _dataService.getEvents();
.collection('events') final eventsData = result['events'] as List<dynamic>;
.get();
final events = <EventModel>[]; final events = <EventModel>[];
// Récupérer toutes les boîtes pour vérifier leur contenu // Récupérer toutes les boîtes pour vérifier leur contenu via l'API
final containersSnapshot = await FirebaseFirestore.instance final containersData = await _dataService.getContainers();
.collection('containers')
.get();
final containersWithEquipment = <String>[]; final containersWithEquipment = <String>[];
for (var containerDoc in containersSnapshot.docs) { for (var containerData in containersData) {
try { try {
final data = containerDoc.data(); final equipmentIds = List<String>.from(containerData['equipmentIds'] ?? []);
final equipmentIds = List<String>.from(data['equipmentIds'] ?? []);
if (equipmentIds.contains(widget.equipment.id)) { if (equipmentIds.contains(widget.equipment.id)) {
containersWithEquipment.add(containerDoc.id); containersWithEquipment.add(containerData['id'] as String);
} }
} catch (e) { } catch (e) {
print('[EquipmentCurrentEventsSection] Error parsing container ${containerDoc.id}: $e'); print('[EquipmentCurrentEventsSection] Error parsing container ${containerData['id']}: $e');
} }
} }
// Filtrer les événements en cours // Filtrer les événements en cours
for (var doc in eventsSnapshot.docs) { for (var eventData in eventsData) {
try { try {
final event = EventModel.fromMap(doc.data(), doc.id); final event = EventModel.fromMap(eventData, eventData['id'] as String);
// Vérifier si l'équipement est directement assigné // Vérifier si l'équipement est directement assigné
final hasEquipmentDirectly = event.assignedEquipment.any( final hasEquipmentDirectly = event.assignedEquipment.any(
@@ -91,7 +89,7 @@ class _EquipmentCurrentEventsSectionState
} }
} }
} catch (e) { } catch (e) {
print('[EquipmentCurrentEventsSection] Error parsing event ${doc.id}: $e'); print('[EquipmentCurrentEventsSection] Error parsing event $eventData: $e');
} }
} }

View File

@@ -1,76 +1,81 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/models/container_model.dart'; import 'package:em2rp/models/container_model.dart';
import 'package:em2rp/providers/container_provider.dart'; import 'package:em2rp/services/container_equipment_service.dart';
import 'package:em2rp/views/container_detail_page.dart';
/// Widget pour afficher les containers parents d'un équipement /// Widget pour afficher les boîtes contenant un équipement
class EquipmentParentContainers extends StatefulWidget { /// Utilise le nouveau système : interroge Firestore via Cloud Function
final List<String> parentBoxIds; class EquipmentParentContainers extends StatelessWidget {
final String equipmentId;
const EquipmentParentContainers({ const EquipmentParentContainers({
super.key, super.key,
required this.parentBoxIds, required this.equipmentId,
}); });
@override
State<EquipmentParentContainers> createState() => _EquipmentParentContainersState();
}
class _EquipmentParentContainersState extends State<EquipmentParentContainers> {
List<ContainerModel> _containers = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadContainers();
}
Future<void> _loadContainers() async {
if (widget.parentBoxIds.isEmpty) {
setState(() {
_isLoading = false;
});
return;
}
setState(() {
_isLoading = true;
});
try {
final containerProvider = context.read<ContainerProvider>();
final List<ContainerModel> containers = [];
for (final boxId in widget.parentBoxIds) {
final container = await containerProvider.getContainerById(boxId);
if (container != null) {
containers.add(container);
}
}
setState(() {
_containers = containers;
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.parentBoxIds.isEmpty) { return FutureBuilder<List<ContainerModel>>(
return const SizedBox.shrink(); future: containerEquipmentService.getContainersByEquipment(equipmentId),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: CircularProgressIndicator(),
),
),
);
}
if (snapshot.hasError) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Icon(Icons.error_outline, color: Colors.red.shade700),
const SizedBox(width: 12),
Expanded(
child: Text(
'Erreur lors du chargement des boîtes',
style: TextStyle(color: Colors.red.shade700),
),
),
],
),
),
);
}
final containers = snapshot.data ?? [];
if (containers.isEmpty) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Icon(Icons.info_outline, color: Colors.grey.shade600),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Cet équipement n\'est dans aucune boîte',
style: TextStyle(color: Colors.grey),
),
),
],
),
),
);
} }
return Card( return Card(
elevation: 2, elevation: 2,
child: Padding( child: Padding(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -78,100 +83,158 @@ class _EquipmentParentContainersState extends State<EquipmentParentContainers> {
children: [ children: [
const Icon(Icons.inventory_2, color: AppColors.rouge, size: 20), const Icon(Icons.inventory_2, color: AppColors.rouge, size: 20),
const SizedBox(width: 8), const SizedBox(width: 8),
const Text( Text(
'Containers', 'Boîtes contenant cet équipement (${containers.length})',
style: TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
], ],
), ),
const Divider(height: 24), const SizedBox(height: 16),
if (_isLoading) ...containers.map((container) => _buildContainerCard(context, container)),
const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
),
)
else if (_containers.isEmpty)
const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'Cet équipement n\'est dans aucun container',
style: TextStyle(color: Colors.grey),
),
),
)
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _containers.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final container = _containers[index];
return _buildContainerTile(container);
},
),
], ],
), ),
), ),
); );
},
);
} }
Widget _buildContainerTile(ContainerModel container) { Widget _buildContainerCard(BuildContext context, ContainerModel container) {
return ListTile( return Padding(
contentPadding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.only(bottom: 12),
leading: Icon( child: Material(
_getTypeIcon(container.type), elevation: 1,
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ContainerDetailPage(container: container),
),
);
},
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
// Icône du type de container
CircleAvatar(
backgroundColor: AppColors.rouge.withValues(alpha: 0.1),
radius: 24,
child: container.type.getIconForAvatar(
size: 24,
color: AppColors.rouge, color: AppColors.rouge,
size: 32,
), ),
title: Text(
container.id,
style: const TextStyle(fontWeight: FontWeight.bold),
), ),
subtitle: Column( const SizedBox(width: 12),
// Informations du container
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(container.name), Text(
container.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
containerTypeLabel(container.type), container.type.label,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.grey.shade600, color: Colors.grey.shade600,
), ),
), ),
const SizedBox(height: 6),
// Badges
Wrap(
spacing: 6,
runSpacing: 4,
children: [
_buildInfoChip(
icon: Icons.inventory,
label: '${container.itemCount} équip.',
color: Colors.blue,
),
if (container.weight != null)
_buildInfoChip(
icon: Icons.scale,
label: '${container.weight!.toStringAsFixed(1)} kg',
color: Colors.orange,
),
_buildInfoChip(
icon: Icons.tag,
label: container.id,
color: Colors.grey,
isCompact: true,
),
], ],
), ),
trailing: const Icon(Icons.chevron_right), ],
onTap: () { ),
Navigator.pushNamed( ),
context,
'/container_detail', // Icône de navigation
arguments: container, Icon(Icons.chevron_right, color: Colors.grey.shade400),
); ],
}, ),
),
),
),
); );
} }
IconData _getTypeIcon(ContainerType type) { Widget _buildInfoChip({
switch (type) { required IconData icon,
case ContainerType.flightCase: required String label,
return Icons.work; required Color color,
case ContainerType.pelicase: bool isCompact = false,
return Icons.work_outline; }) {
case ContainerType.bag: return Container(
return Icons.shopping_bag; padding: EdgeInsets.symmetric(
case ContainerType.openCrate: horizontal: isCompact ? 6 : 8,
return Icons.inventory_2; vertical: 3,
case ContainerType.toolbox: ),
return Icons.handyman; decoration: BoxDecoration(
} color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: color.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: isCompact ? 10 : 12,
color: color.withValues(alpha: 0.8),
),
const SizedBox(width: 3),
Text(
label,
style: TextStyle(
fontSize: isCompact ? 9 : 11,
fontWeight: FontWeight.w600,
color: color.withValues(alpha: 0.9),
),
),
],
),
);
} }
} }

View File

@@ -1,12 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:em2rp/models/container_model.dart'; import 'package:em2rp/models/container_model.dart';
import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/services/container_equipment_service.dart';
import 'package:em2rp/services/container_service.dart';
import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/views/container_detail_page.dart'; import 'package:em2rp/views/container_detail_page.dart';
/// Widget pour afficher les containers qui référencent un équipement /// Widget pour afficher les containers qui référencent un équipement
class EquipmentReferencingContainers extends StatefulWidget { /// Utilise le nouveau système : interroge Firestore via Cloud Function
class EquipmentReferencingContainers extends StatelessWidget {
final String equipmentId; final String equipmentId;
const EquipmentReferencingContainers({ const EquipmentReferencingContainers({
@@ -14,41 +14,35 @@ class EquipmentReferencingContainers extends StatefulWidget {
required this.equipmentId, required this.equipmentId,
}); });
@override
State<EquipmentReferencingContainers> createState() => _EquipmentReferencingContainersState();
}
class _EquipmentReferencingContainersState extends State<EquipmentReferencingContainers> {
final ContainerService _containerService = ContainerService();
List<ContainerModel> _referencingContainers = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadReferencingContainers();
}
Future<void> _loadReferencingContainers() async {
try {
final containers = await _containerService.findContainersWithEquipment(widget.equipmentId);
setState(() {
_referencingContainers = containers;
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_referencingContainers.isEmpty && !_isLoading) { return FutureBuilder<List<ContainerModel>>(
future: containerEquipmentService.getContainersByEquipment(equipmentId),
builder: (context, snapshot) {
// Ne rien afficher si vide et pas en chargement
if (snapshot.connectionState == ConnectionState.done &&
(!snapshot.hasData || snapshot.data!.isEmpty)) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
if (snapshot.connectionState == ConnectionState.waiting) {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(20),
child: Center(
child: CircularProgressIndicator(),
),
),
);
}
if (snapshot.hasError) {
return const SizedBox.shrink(); // Masquer en cas d'erreur
}
final containers = snapshot.data ?? [];
return Card( return Card(
elevation: 2, elevation: 2,
child: Padding( child: Padding(
@@ -62,7 +56,7 @@ class _EquipmentReferencingContainersState extends State<EquipmentReferencingCon
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
'Containers contenant cet équipement', 'Boîtes contenant cet équipement (${containers.length})',
style: Theme.of(context).textTheme.titleLarge?.copyWith( style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -71,218 +65,156 @@ class _EquipmentReferencingContainersState extends State<EquipmentReferencingCon
], ],
), ),
const Divider(height: 24), const Divider(height: 24),
if (_isLoading) _buildContainersGrid(context, containers),
const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
),
)
else if (_referencingContainers.isEmpty)
const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Text(
'Cet équipement n\'est dans aucun container',
style: TextStyle(color: Colors.grey),
),
),
)
else
_buildContainersGrid(),
], ],
), ),
), ),
); );
}
Widget _buildContainersGrid() {
final screenWidth = MediaQuery.of(context).size.width;
final isMobile = screenWidth < 800;
final isTablet = screenWidth < 1200;
final crossAxisCount = isMobile ? 1 : (isTablet ? 2 : 3);
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 12,
mainAxisSpacing: 8,
childAspectRatio: 7.5,
),
itemCount: _referencingContainers.length,
itemBuilder: (context, index) {
final container = _referencingContainers[index];
return _buildContainerCard(container);
}, },
); );
} }
Widget _buildContainerCard(ContainerModel container) { Widget _buildContainersGrid(BuildContext context, List<ContainerModel> containers) {
return Column(
children: containers.map((container) {
return _buildContainerCard(context, container);
}).toList(),
);
}
Widget _buildContainerCard(BuildContext context, ContainerModel container) {
return Card( return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 1, elevation: 1,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.grey.shade200, width: 1),
), ),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ContainerDetailPage(container: container), builder: (_) => ContainerDetailPage(container: container),
), ),
); );
}, },
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(12),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), padding: const EdgeInsets.all(16),
child: Row( child: Row(
children: [ children: [
// Icône du type de container // Icône du type de container
container.type.getIcon(size: 28, color: AppColors.rouge), CircleAvatar(
const SizedBox(width: 10), backgroundColor: AppColors.rouge.withValues(alpha: 0.1),
// Infos textuelles radius: 28,
child: container.type.getIconForAvatar(
size: 28,
color: AppColors.rouge,
),
),
const SizedBox(width: 16),
// Informations du container
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text(
container.id,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
height: 1.0,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
if (container.notes != null && container.notes!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
container.notes!,
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
height: 1.0,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
)
else
Text( Text(
container.name, container.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
container.type.label,
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 13,
color: Colors.grey[600], color: Colors.grey.shade600,
height: 1.0,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
), ),
), ),
const SizedBox(width: 8), const SizedBox(height: 6),
// Badges compacts
Column( // Badges d'information
mainAxisAlignment: MainAxisAlignment.center, Wrap(
crossAxisAlignment: CrossAxisAlignment.end, spacing: 8,
runSpacing: 4,
children: [ children: [
_buildStatusBadge(_getStatusLabel(container.status), _getStatusColor(container.status)), _buildInfoChip(
if (container.itemCount > 0) icon: Icons.inventory,
Padding( label: '${container.itemCount} équipement${container.itemCount > 1 ? 's' : ''}',
padding: const EdgeInsets.only(top: 2), color: Colors.blue,
child: _buildCountBadge(container.itemCount), ),
if (container.weight != null)
_buildInfoChip(
icon: Icons.scale,
label: '${container.weight!.toStringAsFixed(1)} kg',
color: Colors.orange,
),
_buildInfoChip(
icon: Icons.tag,
label: container.id,
color: Colors.grey,
isCompact: true,
), ),
], ],
), ),
], ],
), ),
), ),
// Icône de navigation
Icon(
Icons.chevron_right,
color: AppColors.rouge,
size: 28,
),
],
),
),
), ),
); );
} }
Widget _buildStatusBadge(String label, Color color) { Widget _buildInfoChip({
required IconData icon,
required String label,
required Color color,
bool isCompact = false,
}) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), padding: EdgeInsets.symmetric(
decoration: BoxDecoration( horizontal: isCompact ? 6 : 8,
color: color.withValues(alpha: 0.15), vertical: isCompact ? 2 : 4,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withValues(alpha: 0.4), width: 0.5),
), ),
child: Text( decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: color.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: isCompact ? 12 : 14,
color: color.withValues(alpha: 0.8),
),
const SizedBox(width: 4),
Text(
label, label,
style: TextStyle( style: TextStyle(
fontSize: 9, fontSize: isCompact ? 10 : 12,
color: color, fontWeight: FontWeight.w600,
fontWeight: FontWeight.bold, color: color.withValues(alpha: 0.9),
height: 1.0,
), ),
overflow: TextOverflow.ellipsis, ),
maxLines: 1, ],
), ),
); );
} }
Widget _buildCountBadge(int count) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.withValues(alpha: 0.4), width: 0.5),
),
child: Text(
'$count article${count > 1 ? 's' : ''}',
style: const TextStyle(
fontSize: 9,
color: Colors.green,
fontWeight: FontWeight.bold,
height: 1.0,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
);
}
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 'En maintenance';
}
}
Color _getStatusColor(EquipmentStatus status) {
switch (status) {
case EquipmentStatus.available:
return Colors.green;
case EquipmentStatus.inUse:
return Colors.blue;
case EquipmentStatus.rented:
return Colors.orange;
case EquipmentStatus.lost:
return Colors.red;
case EquipmentStatus.outOfService:
return Colors.red;
case EquipmentStatus.maintenance:
return Colors.yellow;
}
}
} }

View File

@@ -0,0 +1,445 @@
import 'package:flutter/material.dart';
import 'package:em2rp/models/container_model.dart';
import 'package:em2rp/utils/colors.dart';
/// Widget pour sélectionner les boîtes parentes d'un équipement
class ParentBoxesSelector extends StatefulWidget {
final List<ContainerModel> availableBoxes;
final List<String> selectedBoxIds;
final Function(List<String>) onSelectionChanged;
const ParentBoxesSelector({
super.key,
required this.availableBoxes,
required this.selectedBoxIds,
required this.onSelectionChanged,
});
@override
State<ParentBoxesSelector> createState() => _ParentBoxesSelectorState();
}
class _ParentBoxesSelectorState extends State<ParentBoxesSelector> {
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
@override
void initState() {
super.initState();
print('[ParentBoxesSelector] initState');
print('[ParentBoxesSelector] Available boxes: ${widget.availableBoxes.length}');
print('[ParentBoxesSelector] Selected box IDs: ${widget.selectedBoxIds}');
// Log détaillé de chaque boîte
for (var box in widget.availableBoxes) {
print('[ParentBoxesSelector] Box - ID: ${box.id}, Name: ${box.name}');
}
}
@override
void didUpdateWidget(ParentBoxesSelector oldWidget) {
super.didUpdateWidget(oldWidget);
print('[ParentBoxesSelector] didUpdateWidget');
print('[ParentBoxesSelector] Old selected: ${oldWidget.selectedBoxIds}');
print('[ParentBoxesSelector] New selected: ${widget.selectedBoxIds}');
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
List<ContainerModel> get _filteredBoxes {
if (_searchQuery.isEmpty) {
return widget.availableBoxes;
}
final query = _searchQuery.toLowerCase();
return widget.availableBoxes.where((box) {
return box.name.toLowerCase().contains(query) ||
box.id.toLowerCase().contains(query) ||
box.type.label.toLowerCase().contains(query);
}).toList();
}
void _toggleSelection(String boxId) {
final newSelection = List<String>.from(widget.selectedBoxIds);
if (newSelection.contains(boxId)) {
newSelection.remove(boxId);
} else {
newSelection.add(boxId);
}
widget.onSelectionChanged(newSelection);
}
@override
Widget build(BuildContext context) {
if (widget.availableBoxes.isEmpty && widget.selectedBoxIds.isEmpty) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Icon(Icons.info_outline, color: Colors.grey.shade600),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Aucune boîte disponible',
style: TextStyle(color: Colors.grey),
),
),
],
),
),
);
}
final filteredBoxes = _filteredBoxes;
final selectedCount = widget.selectedBoxIds.length;
// Vérifier s'il y a des boîtes sélectionnées qui ne sont pas dans la liste
final missingBoxIds = widget.selectedBoxIds
.where((id) => !widget.availableBoxes.any((box) => box.id == id))
.toList();
return Card(
elevation: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec titre et compteur
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
const Icon(Icons.inventory_2, color: AppColors.rouge, size: 20),
const SizedBox(width: 8),
const Text(
'Boîtes parentes',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
if (selectedCount > 0)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: AppColors.rouge.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.rouge.withValues(alpha: 0.3),
width: 1,
),
),
child: Text(
'$selectedCount sélectionné${selectedCount > 1 ? 's' : ''}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.rouge,
),
),
),
],
),
),
const Divider(height: 1),
// Message d'avertissement si des boîtes sélectionnées sont manquantes
if (missingBoxIds.isNotEmpty)
Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.shade300),
),
child: Row(
children: [
Icon(Icons.warning_amber, color: Colors.orange.shade700),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Boîtes introuvables',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.orange.shade900,
),
),
const SizedBox(height: 4),
Text(
'Les boîtes suivantes sont sélectionnées mais n\'existent plus : ${missingBoxIds.join(", ")}',
style: TextStyle(
fontSize: 13,
color: Colors.orange.shade800,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
// Retirer les boîtes manquantes de la sélection
final newSelection = widget.selectedBoxIds
.where((id) => !missingBoxIds.contains(id))
.toList();
widget.onSelectionChanged(newSelection);
},
tooltip: 'Retirer ces boîtes',
),
],
),
),
// Barre de recherche
Padding(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _searchController,
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
decoration: InputDecoration(
hintText: 'Rechercher par nom, ID ou type...',
prefixIcon: const Icon(Icons.search, color: AppColors.rouge),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
setState(() {
_searchController.clear();
_searchQuery = '';
});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColors.rouge, width: 2),
),
filled: true,
fillColor: Colors.grey.shade50,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
),
),
// Message si aucun résultat
if (filteredBoxes.isEmpty)
Padding(
padding: const EdgeInsets.all(32.0),
child: Center(
child: Column(
children: [
Icon(Icons.search_off, size: 48, color: Colors.grey.shade400),
const SizedBox(height: 12),
Text(
'Aucune boîte trouvée',
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
'Essayez une autre recherche',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
],
),
),
)
else
// Liste des boîtes
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
itemCount: filteredBoxes.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final box = filteredBoxes[index];
final isSelected = widget.selectedBoxIds.contains(box.id);
if (index == 0) {
print('[ParentBoxesSelector] Building item $index');
print('[ParentBoxesSelector] Box ID: ${box.id}');
print('[ParentBoxesSelector] Selected IDs: ${widget.selectedBoxIds}');
print('[ParentBoxesSelector] Is selected: $isSelected');
}
return _buildBoxCard(box, isSelected);
},
),
const SizedBox(height: 16),
],
),
);
}
Widget _buildBoxCard(ContainerModel box, bool isSelected) {
return Card(
elevation: isSelected ? 3 : 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: isSelected ? AppColors.rouge : Colors.grey.shade300,
width: isSelected ? 2 : 1,
),
),
child: InkWell(
onTap: () => _toggleSelection(box.id),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
// Checkbox
Checkbox(
value: isSelected,
onChanged: (value) => _toggleSelection(box.id),
activeColor: AppColors.rouge,
),
const SizedBox(width: 8),
// Icône du type de container
CircleAvatar(
backgroundColor: isSelected
? AppColors.rouge.withValues(alpha: 0.15)
: Colors.grey.shade200,
radius: 24,
child: box.type.getIconForAvatar(
size: 24,
color: isSelected ? AppColors.rouge : Colors.grey.shade700,
),
),
const SizedBox(width: 12),
// Informations de la boîte
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
box.name,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
color: isSelected ? AppColors.rouge : Colors.black87,
),
),
const SizedBox(height: 4),
Text(
box.type.label,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 6),
// Badges
Wrap(
spacing: 6,
runSpacing: 4,
children: [
_buildInfoChip(
icon: Icons.inventory,
label: '${box.itemCount} équip.',
color: Colors.blue,
),
if (box.weight != null)
_buildInfoChip(
icon: Icons.scale,
label: '${box.weight!.toStringAsFixed(1)} kg',
color: Colors.orange,
),
_buildInfoChip(
icon: Icons.tag,
label: box.id,
color: Colors.grey,
isCompact: true,
),
],
),
],
),
),
// Indicateur de sélection
if (isSelected)
const Icon(
Icons.check_circle,
color: AppColors.rouge,
size: 24,
),
],
),
),
),
);
}
Widget _buildInfoChip({
required IconData icon,
required String label,
required Color color,
bool isCompact = false,
}) {
return Container(
padding: EdgeInsets.symmetric(
horizontal: isCompact ? 6 : 8,
vertical: 3,
),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: color.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: isCompact ? 10 : 12,
color: color.withValues(alpha: 0.8),
),
const SizedBox(width: 3),
Text(
label,
style: TextStyle(
fontSize: isCompact ? 9 : 11,
fontWeight: FontWeight.w600,
color: color.withValues(alpha: 0.9),
),
),
],
),
);
}
}

View File

@@ -189,11 +189,14 @@ class _RestockDialogState extends State<RestockDialog> {
}; };
if (context.mounted) { if (context.mounted) {
await context.read<EquipmentProvider>().updateEquipment( final updatedEquipment = widget.equipment.copyWith(
widget.equipment.id, availableQuantity: newAvailable,
updatedData, totalQuantity: newTotal,
updatedAt: DateTime.now(),
); );
await context.read<EquipmentProvider>().updateEquipment(updatedEquipment);
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(

View File

@@ -34,8 +34,6 @@ class EventAssignedEquipmentSection extends StatefulWidget {
} }
class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSection> { class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSection> {
// ...existing code...
bool get _canAddMaterial => widget.startDate != null && widget.endDate != null; bool get _canAddMaterial => widget.startDate != null && widget.endDate != null;
final EventAvailabilityService _availabilityService = EventAvailabilityService(); final EventAvailabilityService _availabilityService = EventAvailabilityService();
Map<String, EquipmentModel> _equipmentCache = {}; Map<String, EquipmentModel> _equipmentCache = {};
@@ -104,7 +102,7 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
_containerCache[containerId] = container; _containerCache[containerId] = container;
} }
} catch (e) { } catch (e) {
print('[EventAssignedEquipmentSection] Error loading equipment/containers: $e'); // Erreur silencieuse - le cache restera vide
} finally { } finally {
setState(() => _isLoading = false); setState(() => _isLoading = false);
} }
@@ -712,7 +710,13 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
return Card( return Card(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
child: ListTile( child: ListTile(
leading: equipment.category.getIcon(size: 24, color: equipment.category.color), leading: CircleAvatar(
backgroundColor: equipment.category.color.withValues(alpha: 0.2),
child: equipment.category.getIconForAvatar(
size: 24,
color: equipment.category.color
),
),
title: Text( title: Text(
equipment.id, equipment.id,
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
@@ -723,7 +727,7 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (isConsumable && eventEq.quantity > 1) if (isConsumable)
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
class EventOptionsDisplayWidget extends StatelessWidget { class EventOptionsDisplayWidget extends StatelessWidget {
final List<Map<String, dynamic>> optionsData; final List<Map<String, dynamic>> optionsData;
@@ -172,31 +173,36 @@ class EventOptionsDisplayWidget extends StatelessWidget {
Future<List<Map<String, dynamic>>> _loadOptionsWithDetails(List<Map<String, dynamic>> optionsData) async { Future<List<Map<String, dynamic>>> _loadOptionsWithDetails(List<Map<String, dynamic>> optionsData) async {
List<Map<String, dynamic>> enrichedOptions = []; List<Map<String, dynamic>> enrichedOptions = [];
// Charger toutes les options via l'API une seule fois
final dataService = DataService(FirebaseFunctionsApiService());
final allOptionsData = await dataService.getOptions();
// Créer une map pour accès rapide par ID
final optionsMap = {
for (var opt in allOptionsData) opt['id']: opt
};
for (final optionData in optionsData) { for (final optionData in optionsData) {
try { try {
// Si l'option a un ID, récupérer les détails complets depuis Firestore // Si l'option a un ID, récupérer les détails complets depuis l'API
if (optionData['id'] != null) { if (optionData['id'] != null) {
final doc = await FirebaseFirestore.instance final apiData = optionsMap[optionData['id']];
.collection('options')
.doc(optionData['id'])
.get();
if (doc.exists) { if (apiData != null) {
final firestoreData = doc.data()!; // Combiner les données API avec le prix choisi
// Combiner les données Firestore avec le prix choisi
enrichedOptions.add({ enrichedOptions.add({
'id': optionData['id'], 'id': optionData['id'],
'code': firestoreData['code'] ?? optionData['id'], // Récupérer le code depuis Firestore 'code': apiData['code'] ?? optionData['id'],
'name': firestoreData['name'], // Récupéré depuis Firestore 'name': apiData['name'],
'details': firestoreData['details'] ?? '', // Récupéré depuis Firestore 'details': apiData['details'] ?? '',
'price': optionData['price'], // Prix choisi par l'utilisateur 'price': optionData['price'], // Prix choisi par l'utilisateur
'quantity': optionData['quantity'] ?? 1, // Quantité 'quantity': optionData['quantity'] ?? 1, // Quantité
'isQuantitative': firestoreData['isQuantitative'] ?? false, 'isQuantitative': apiData['isQuantitative'] ?? false,
'valMin': firestoreData['valMin'], 'valMin': apiData['valMin'],
'valMax': firestoreData['valMax'], 'valMax': apiData['valMax'],
}); });
} else { } else {
// Option supprimée de Firestore, afficher avec des données par défaut // Option supprimée, afficher avec des données par défaut
enrichedOptions.add({ enrichedOptions.add({
'id': optionData['id'], 'id': optionData['id'],
'name': 'Option supprimée (ID: ${optionData['id']})', 'name': 'Option supprimée (ID: ${optionData['id']})',

View File

@@ -1,16 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:provider/provider.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:em2rp/providers/users_provider.dart';
class ProfilePictureWidget extends StatefulWidget { class ProfilePictureWidget extends StatefulWidget {
final String? userId; final String? userId;
final double radius; final double radius;
final String? defaultImageUrl; final String? defaultImageUrl;
final String? profilePhotoUrl; // URL directe de la photo (optionnel)
const ProfilePictureWidget({ const ProfilePictureWidget({
super.key, super.key,
required this.userId, this.userId,
this.radius = 25, this.radius = 20,
this.defaultImageUrl, this.defaultImageUrl,
this.profilePhotoUrl, // Si fourni, utilisé directement sans appeler UsersProvider
}); });
@override @override
@@ -18,110 +22,56 @@ class ProfilePictureWidget extends StatefulWidget {
} }
class _ProfilePictureWidgetState extends State<ProfilePictureWidget> { class _ProfilePictureWidgetState extends State<ProfilePictureWidget> {
late Future<DocumentSnapshot?> _userFuture;
@override
void initState() {
super.initState();
_userFuture = _getUserFuture();
}
@override
void didUpdateWidget(ProfilePictureWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.userId != widget.userId) {
_userFuture = _getUserFuture();
}
}
Future<DocumentSnapshot?> _getUserFuture() {
if (widget.userId == null || widget.userId!.isEmpty) {
return Future.value(null);
}
return FirebaseFirestore.instance
.collection('users')
.doc(widget.userId)
.get();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Si profilePhotoUrl est fourni directement, l'utiliser sans appeler le provider
if (widget.profilePhotoUrl != null && widget.profilePhotoUrl!.isNotEmpty) {
return CircleAvatar(
radius: widget.radius,
backgroundImage: CachedNetworkImageProvider(widget.profilePhotoUrl!),
onBackgroundImageError: (_, __) {
// En cas d'erreur, afficher l'image par défaut
},
);
}
if (widget.userId == null || widget.userId!.isEmpty) { if (widget.userId == null || widget.userId!.isEmpty) {
return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl); return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
} }
return FutureBuilder<DocumentSnapshot?>( // Utiliser le provider pour récupérer l'utilisateur
future: _userFuture, final usersProvider = context.watch<UsersProvider>();
builder: (context, snapshot) { final user = usersProvider.getUserById(widget.userId!);
if (snapshot.connectionState == ConnectionState.waiting) {
return _buildLoadingAvatar(widget.radius); if (user == null) {
} else if (snapshot.hasError) { return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
print("Error loading profile: ${snapshot.error}"); }
return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
} else if (snapshot.data != null && snapshot.data!.exists) { final profilePhotoUrl = user.profilePhotoUrl;
final userData = snapshot.data!.data() as Map<String, dynamic>?;
final profilePhotoUrl = userData?['profilePhotoUrl'] as String?; if (profilePhotoUrl.isEmpty) {
return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
}
if (profilePhotoUrl != null && profilePhotoUrl.isNotEmpty) {
return CircleAvatar( return CircleAvatar(
radius: widget.radius, radius: widget.radius,
backgroundImage: NetworkImage(profilePhotoUrl), backgroundImage: CachedNetworkImageProvider(profilePhotoUrl),
onBackgroundImageError: (e, stack) { onBackgroundImageError: (_, __) {
print("Error loading profile image: $e"); // En cas d'erreur, afficher l'image par défaut
},
);
}
}
return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
}, },
); );
} }
// Widget utilitaire pour construire un CircleAvatar de chargement
Widget _buildLoadingAvatar(double radius) {
return CircleAvatar(
radius: radius,
backgroundColor:
Colors.grey[300], // Couleur de fond pendant le chargement
child: SizedBox(
width: radius * 0.8, // Ajuster la taille du loader
height: radius * 0.8,
child: const CircularProgressIndicator(
strokeWidth: 2), // Indicateur de chargement
),
);
}
// Widget utilitaire pour construire un CircleAvatar par défaut (avec icône ou image par défaut)
Widget _buildDefaultAvatar(double radius, String? defaultImageUrl) { Widget _buildDefaultAvatar(double radius, String? defaultImageUrl) {
if (defaultImageUrl != null && defaultImageUrl.isNotEmpty) {
return CircleAvatar( return CircleAvatar(
radius: radius, radius: radius,
// Utilisation de Image.network pour l'image par défaut, avec gestion d'erreur similaire backgroundImage: defaultImageUrl != null && defaultImageUrl.isNotEmpty
backgroundImage: Image.network( ? CachedNetworkImageProvider(defaultImageUrl)
defaultImageUrl, : null,
errorBuilder: (context, error, stackTrace) { child: defaultImageUrl == null || defaultImageUrl.isEmpty
print( ? Icon(Icons.person, size: radius)
"Erreur de chargement Image.network pour l'URL par défaut: $defaultImageUrl, Erreur: $error"); : null,
return _buildIconAvatar(
radius); // Si l'image par défaut ne charge pas, afficher l'icône
},
).image, // .image pour ImageProvider
);
} else {
return _buildIconAvatar(
radius); // Si pas d'URL par défaut fournie, afficher l'icône
}
}
// Widget utilitaire pour construire un CircleAvatar avec une icône par défaut
Widget _buildIconAvatar(double radius) {
return CircleAvatar(
radius: radius,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Icon(Icons.account_circle, size: radius * 1.5),
),
); );
} }
} }

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/option_model.dart'; import 'package:em2rp/models/option_model.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
class OptionSelectorWidget extends StatefulWidget { class OptionSelectorWidget extends StatefulWidget {
final List<Map<String, dynamic>> selectedOptions; final List<Map<String, dynamic>> selectedOptions;
@@ -42,15 +43,20 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
Future<void> _fetchOptions() async { Future<void> _fetchOptions() async {
setState(() => _loading = true); setState(() => _loading = true);
final snapshot = try {
await FirebaseFirestore.instance.collection('options').get(); final dataService = DataService(FirebaseFunctionsApiService());
final options = snapshot.docs final optionsData = await dataService.getOptions();
.map((doc) => EventOption.fromMap(doc.data(), doc.id)) final options = optionsData
.map((data) => EventOption.fromMap(data, data['id'] as String))
.toList(); .toList();
setState(() { setState(() {
_allOptions = options; _allOptions = options;
_loading = false; _loading = false;
}); });
} catch (e) {
setState(() => _loading = false);
// Afficher une erreur silencieuse
}
} }
// Méthode publique pour mettre à jour les options depuis l'extérieur // Méthode publique pour mettre à jour les options depuis l'extérieur
@@ -258,27 +264,32 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
); );
} }
// Méthode pour charger les détails des options depuis Firebase // Méthode pour charger les détails des options via l'API
Future<List<Map<String, dynamic>>> _loadOptionsWithDetails(List<Map<String, dynamic>> optionsData) async { Future<List<Map<String, dynamic>>> _loadOptionsWithDetails(List<Map<String, dynamic>> optionsData) async {
List<Map<String, dynamic>> enrichedOptions = []; List<Map<String, dynamic>> enrichedOptions = [];
// Charger toutes les options via l'API
final dataService = DataService(FirebaseFunctionsApiService());
final allOptionsData = await dataService.getOptions();
// Créer une map pour accès rapide par ID
final optionsMap = {
for (var opt in allOptionsData) opt['id']: opt
};
for (final optionData in optionsData) { for (final optionData in optionsData) {
try { try {
// Si l'option a un ID, récupérer les détails depuis Firestore // Si l'option a un ID, récupérer les détails depuis l'API
if (optionData['id'] != null) { if (optionData['id'] != null) {
final doc = await FirebaseFirestore.instance final firestoreData = optionsMap[optionData['id']];
.collection('options')
.doc(optionData['id'])
.get();
if (doc.exists) { if (firestoreData != null) {
final firestoreData = doc.data()!;
enrichedOptions.add({ enrichedOptions.add({
'id': optionData['id'], 'id': optionData['id'],
'code': firestoreData['code'] ?? optionData['id'], // Récupérer le code 'code': firestoreData['code'] ?? optionData['id'],
'name': firestoreData['code'] != null && firestoreData['code'].toString().isNotEmpty 'name': firestoreData['code'] != null && firestoreData['code'].toString().isNotEmpty
? '${firestoreData['code']} - ${firestoreData['name']}' ? '${firestoreData['code']} - ${firestoreData['name']}'
: firestoreData['name'], // Affichage avec code : firestoreData['name'],
'details': firestoreData['details'] ?? '', 'details': firestoreData['details'] ?? '',
'price': optionData['price'], 'price': optionData['price'],
'quantity': optionData['quantity'] ?? 1, 'quantity': optionData['quantity'] ?? 1,
@@ -347,9 +358,11 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> {
} }
Future<void> _reloadOptions() async { Future<void> _reloadOptions() async {
final snapshot = await FirebaseFirestore.instance.collection('options').get(); try {
final updatedOptions = snapshot.docs final dataService = DataService(FirebaseFunctionsApiService());
.map((doc) => EventOption.fromMap(doc.data(), doc.id)) final optionsData = await dataService.getOptions();
final updatedOptions = optionsData
.map((data) => EventOption.fromMap(data, data['id'] as String))
.toList(); .toList();
setState(() { setState(() {
@@ -358,6 +371,9 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> {
// Appeler le callback pour mettre à jour aussi le parent // Appeler le callback pour mettre à jour aussi le parent
widget.onOptionsUpdated?.call(updatedOptions); widget.onOptionsUpdated?.call(updatedOptions);
} catch (e) {
// Erreur silencieuse
}
} }
@override @override
@@ -557,23 +573,34 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
bool _loading = true; bool _loading = true;
Future<bool> _isCodeUnique(String code) async { Future<bool> _isCodeUnique(String code) async {
// Vérifier si le document avec ce code existe déjà try {
final doc = await FirebaseFirestore.instance // Charger toutes les options via l'API
.collection('options') final dataService = DataService(FirebaseFunctionsApiService());
.doc(code) final optionsData = await dataService.getOptions();
.get();
return !doc.exists; // Vérifier si le code existe déjà
return !optionsData.any((opt) => opt['id'] == code);
} catch (e) {
return false;
}
} }
Future<void> _fetchEventTypes() async { Future<void> _fetchEventTypes() async {
setState(() { setState(() {
_loading=true; _loading = true;
}); });
final snapshot = await FirebaseFirestore.instance.collection('eventTypes').get(); try {
final dataService = DataService(FirebaseFunctionsApiService());
final eventTypesData = await dataService.getEventTypes();
setState(() { setState(() {
_allEventTypes = snapshot.docs.map((doc) => {'id': doc.id, 'name': doc['name']}).toList(); _allEventTypes = eventTypesData
.map((data) => {'id': data['id'], 'name': data['name']})
.toList();
_loading = false; _loading = false;
}); });
} catch (e) {
setState(() => _loading = false);
}
} }
@override @override
@@ -741,8 +768,9 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
return; return;
} }
try { try {
// Utiliser le code comme identifiant du document // Créer via l'API
await FirebaseFirestore.instance.collection('options').doc(code).set({ final dataService = DataService(FirebaseFunctionsApiService());
await dataService.createOption(code, {
'code': code, 'code': code,
'name': name, 'name': name,
'details': _detailsController.text.trim(), 'details': _detailsController.text.trim(),
@@ -751,7 +779,9 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
'eventTypes': _selectedTypes, 'eventTypes': _selectedTypes,
'isQuantitative': _isQuantitative, 'isQuantitative': _isQuantitative,
}); });
if (mounted) {
Navigator.pop(context, true); Navigator.pop(context, true);
}
} catch (e) { } catch (e) {
setState(() => _error = 'Erreur lors de la création : $e'); setState(() => _error = 'Erreur lors de la création : $e');
} }

View File

@@ -3,8 +3,9 @@ import 'package:provider/provider.dart';
import 'package:em2rp/models/user_model.dart'; import 'package:em2rp/models/user_model.dart';
import 'package:em2rp/providers/users_provider.dart'; import 'package:em2rp/providers/users_provider.dart';
import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/colors.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/models/role_model.dart'; import 'package:em2rp/models/role_model.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
class EditUserDialog extends StatefulWidget { class EditUserDialog extends StatefulWidget {
final UserModel user; final UserModel user;
@@ -34,16 +35,21 @@ class _EditUserDialogState extends State<EditUserDialog> {
} }
Future<void> _loadRoles() async { Future<void> _loadRoles() async {
final snapshot = await FirebaseFirestore.instance.collection('roles').get(); try {
final dataService = DataService(FirebaseFunctionsApiService());
final rolesData = await dataService.getRoles();
setState(() { setState(() {
availableRoles = snapshot.docs availableRoles = rolesData
.map((doc) => RoleModel.fromMap(doc.data(), doc.id)) .map((data) => RoleModel.fromMap(data, data['id'] as String))
.toList(); .toList();
selectedRoleId = widget.user.role.isEmpty selectedRoleId = widget.user.role.isEmpty
? (availableRoles.isNotEmpty ? availableRoles.first.id : null) ? (availableRoles.isNotEmpty ? availableRoles.first.id : null)
: widget.user.role; : widget.user.role;
isLoadingRoles = false; isLoadingRoles = false;
}); });
} catch (e) {
setState(() => isLoadingRoles = false);
}
} }
@override @override
@@ -176,7 +182,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
role: selectedRoleId, role: selectedRoleId,
); );
await Provider.of<UsersProvider>(context, listen: false) await Provider.of<UsersProvider>(context, listen: false)
.updateUser(updatedUser, roleId: selectedRoleId); .updateUser(updatedUser);
Navigator.pop(context); Navigator.pop(context);
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(

View File

@@ -200,7 +200,10 @@ class UserChipsList extends StatelessWidget {
children: users children: users
.map((user) => Chip( .map((user) => Chip(
avatar: ProfilePictureWidget( avatar: ProfilePictureWidget(
userId: user.uid, radius: avatarRadius), userId: user.uid,
radius: avatarRadius,
profilePhotoUrl: user.profilePhotoUrl, // Passer l'URL directement
),
label: Text('${user.firstName} ${user.lastName}', label: Text('${user.firstName} ${user.lastName}',
style: const TextStyle(fontSize: 16)), style: const TextStyle(fontSize: 16)),
labelPadding: const EdgeInsets.symmetric(horizontal: 8), labelPadding: const EdgeInsets.symmetric(horizontal: 8),