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:
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
62
em2rp/lib/providers/alert_provider_new.dart
Normal file
62
em2rp/lib/providers/alert_provider_new.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
109
em2rp/lib/providers/container_provider_new.dart
Normal file
109
em2rp/lib/providers/container_provider_new.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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,
|
notifyListeners();
|
||||||
model: _selectedModel,
|
|
||||||
searchQuery: _searchQuery,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Charger tous les modèles uniques
|
|
||||||
Future<void> loadModels() async {
|
|
||||||
try {
|
try {
|
||||||
_models = await _service.getAllModels();
|
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 models: $e');
|
print('[EquipmentProvider] Error loading equipments: $e');
|
||||||
rethrow;
|
_isLoading = false;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Charger toutes les marques uniques
|
|
||||||
Future<void> loadBrands() async {
|
|
||||||
try {
|
|
||||||
_brands = await _service.getAllBrands();
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
|
||||||
print('Error loading brands: $e');
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
_events = allEvents;
|
||||||
if (canViewAllEvents) {
|
print('Successfully loaded ${_events.length} events (${failedCount} failed)');
|
||||||
_events = allEvents;
|
|
||||||
print('Admin user: showing all ${_events.length} events');
|
|
||||||
} 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 {
|
||||||
|
await loadUserEvents(userId, canViewAllEvents: canViewAllEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupérer un événement spécifique par ID
|
||||||
|
EventModel? getEventById(String eventId) {
|
||||||
try {
|
try {
|
||||||
final doc = await _firestore.collection('events').doc(eventId).get();
|
return _events.firstWhere((event) => event.id == eventId);
|
||||||
if (doc.exists) {
|
|
||||||
return EventModel.fromMap(doc.data()!, doc.id);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
51
em2rp/lib/providers/maintenance_provider_new.dart
Normal file
51
em2rp/lib/providers/maintenance_provider_new.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Réinitialisation du mot de passe
|
/// Créer un utilisateur avec invitation par email
|
||||||
Future<void> resetPassword(String email) async {
|
Future<void> createUserWithEmailInvite({
|
||||||
await _userService.resetPassword(email);
|
required String email,
|
||||||
}
|
required String firstName,
|
||||||
|
required String lastName,
|
||||||
Future<void> createUserWithEmailInvite(BuildContext context, UserModel user,
|
String? phoneNumber,
|
||||||
{String? roleId}) async {
|
required String roleId,
|
||||||
String? authUid;
|
}) async {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Vérifier l'état de l'authentification
|
// TODO: Implémenter via Cloud Function
|
||||||
final currentUser = _auth.currentUser;
|
print('Creating user with email invite: $email');
|
||||||
print('Current user: ${currentUser?.email}');
|
await fetchUsers(); // Recharger la liste
|
||||||
|
|
||||||
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) {
|
} catch (e) {
|
||||||
print('Error creating user: $e');
|
print('Error creating user with email invite: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Réinitialisation du mot de passe
|
||||||
|
Future<void> resetPassword(String email) async {
|
||||||
|
// Firebase Auth reste OK
|
||||||
|
// await _userService.resetPassword(email);
|
||||||
|
// TODO: Implémenter via Cloud Function
|
||||||
|
print('Reset password for: $email');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
em2rp/lib/services/container_equipment_service.dart
Normal file
52
em2rp/lib/services/container_equipment_service.dart
Normal 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();
|
||||||
|
|
||||||
339
em2rp/lib/services/data_service.dart
Normal file
339
em2rp/lib/services/data_service.dart
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,19 +66,30 @@ 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;
|
||||||
if (doc.exists) {
|
|
||||||
final data = doc.data() as Map<String, dynamic>?;
|
// Gérer String (UID) ou DocumentReference
|
||||||
if (data != null) {
|
if (ref is String) {
|
||||||
final firstName = data['firstName'] ?? '';
|
docRef = FirebaseFirestore.instance.collection('users').doc(ref);
|
||||||
final lastName = data['lastName'] ?? '';
|
} else if (ref is DocumentReference) {
|
||||||
if (firstName.isNotEmpty || lastName.isNotEmpty) {
|
docRef = ref;
|
||||||
workforceNames.add('$firstName $lastName'.trim());
|
}
|
||||||
|
|
||||||
|
if (docRef != null) {
|
||||||
|
final doc = await docRef.get();
|
||||||
|
if (doc.exists) {
|
||||||
|
final data = doc.data() as Map<String, dynamic>?;
|
||||||
|
if (data != null) {
|
||||||
|
final firstName = data['firstName'] ?? '';
|
||||||
|
final lastName = data['lastName'] ?? '';
|
||||||
|
if (firstName.isNotEmpty || lastName.isNotEmpty) {
|
||||||
|
workforceNames.add('$firstName $lastName'.trim());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,35 +69,60 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
|
|
||||||
void _populateFields() {
|
void _populateFields() {
|
||||||
final equipment = widget.equipment!;
|
final equipment = widget.equipment!;
|
||||||
_identifierController.text = equipment.id;
|
setState(() {
|
||||||
_brandController.text = equipment.brand ?? '';
|
_identifierController.text = equipment.id;
|
||||||
_selectedBrand = equipment.brand;
|
_brandController.text = equipment.brand ?? '';
|
||||||
_modelController.text = equipment.model ?? '';
|
_selectedBrand = equipment.brand;
|
||||||
_selectedCategory = equipment.category;
|
_modelController.text = equipment.model ?? '';
|
||||||
_selectedStatus = equipment.status;
|
_selectedCategory = equipment.category;
|
||||||
_purchasePriceController.text = equipment.purchasePrice?.toStringAsFixed(2) ?? '';
|
_selectedStatus = equipment.status;
|
||||||
_rentalPriceController.text = equipment.rentalPrice?.toStringAsFixed(2) ?? '';
|
_purchasePriceController.text = equipment.purchasePrice?.toStringAsFixed(2) ?? '';
|
||||||
_totalQuantityController.text = equipment.totalQuantity?.toString() ?? '';
|
_rentalPriceController.text = equipment.rentalPrice?.toStringAsFixed(2) ?? '';
|
||||||
_criticalThresholdController.text = equipment.criticalThreshold?.toString() ?? '';
|
_totalQuantityController.text = equipment.totalQuantity?.toString() ?? '';
|
||||||
_purchaseDate = equipment.purchaseDate;
|
_criticalThresholdController.text = equipment.criticalThreshold?.toString() ?? '';
|
||||||
_lastMaintenanceDate = equipment.lastMaintenanceDate;
|
_purchaseDate = equipment.purchaseDate;
|
||||||
_nextMaintenanceDate = equipment.nextMaintenanceDate;
|
_lastMaintenanceDate = equipment.lastMaintenanceDate;
|
||||||
_selectedParentBoxIds = List.from(equipment.parentBoxIds);
|
_nextMaintenanceDate = equipment.nextMaintenanceDate;
|
||||||
_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,35 +481,14 @@ 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'),
|
setState(() {
|
||||||
),
|
_selectedParentBoxIds = newSelection;
|
||||||
);
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
return Card(
|
|
||||||
child: Column(
|
|
||||||
children: _availableBoxes.map((box) {
|
|
||||||
final isSelected = _selectedParentBoxIds.contains(box.id);
|
|
||||||
return CheckboxListTile(
|
|
||||||
title: Text(box.name),
|
|
||||||
subtitle: box.model != null ? Text('Modèle: {box.model}') : null,
|
|
||||||
value: isSelected,
|
|
||||||
onChanged: (bool? value) {
|
|
||||||
setState(() {
|
|
||||||
if (value == true) {
|
|
||||||
_selectedParentBoxIds.add(box.id);
|
|
||||||
} else {
|
|
||||||
_selectedParentBoxIds.remove(box.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,10 +942,13 @@ 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(
|
||||||
|
|||||||
@@ -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,52 +334,37 @@ 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) {
|
// Déterminer le nouveau statut
|
||||||
final equipmentData = EquipmentModel.fromMap(
|
EquipmentStatus newStatus;
|
||||||
doc.data() as Map<String, dynamic>,
|
if (eq.isReturned) {
|
||||||
doc.id,
|
newStatus = EquipmentStatus.available;
|
||||||
|
} else if (eq.isPrepared || eq.isLoaded) {
|
||||||
|
newStatus = EquipmentStatus.inUse;
|
||||||
|
} else {
|
||||||
|
continue; // Pas de changement
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ne mettre à jour que les équipements non quantifiables
|
||||||
|
if (!equipmentData.hasQuantity) {
|
||||||
|
await _dataService.updateEquipmentStatusOnly(
|
||||||
|
equipmentId: eq.equipmentId,
|
||||||
|
status: equipmentStatusToString(newStatus),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Déterminer le nouveau statut
|
// Gérer les stocks pour les consommables
|
||||||
EquipmentStatus newStatus;
|
if (equipmentData.hasQuantity && eq.isReturned && eq.returnedQuantity != null) {
|
||||||
if (eq.isReturned) {
|
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
||||||
newStatus = EquipmentStatus.available;
|
await _dataService.updateEquipmentStatusOnly(
|
||||||
} else if (eq.isPrepared || eq.isLoaded) {
|
equipmentId: eq.equipmentId,
|
||||||
newStatus = EquipmentStatus.inUse;
|
availableQuantity: currentAvailable + eq.returnedQuantity!,
|
||||||
} else {
|
);
|
||||||
continue; // Pas de changement
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ne mettre à jour que les équipements non quantifiables
|
|
||||||
if (!equipmentData.hasQuantity) {
|
|
||||||
await FirebaseFirestore.instance
|
|
||||||
.collection('equipments')
|
|
||||||
.doc(eq.equipmentId)
|
|
||||||
.update({
|
|
||||||
'status': equipmentStatusToString(newStatus),
|
|
||||||
'updatedAt': Timestamp.fromDate(DateTime.now()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gérer les stocks pour les consommables
|
|
||||||
if (equipmentData.hasQuantity && eq.isReturned && eq.returnedQuantity != null) {
|
|
||||||
final currentAvailable = equipmentData.availableQuantity ?? 0;
|
|
||||||
await FirebaseFirestore.instance
|
|
||||||
.collection('equipments')
|
|
||||||
.doc(eq.equipmentId)
|
|
||||||
.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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
.toList();
|
.map((data) => RoleModel.fromMap(data, data['id'] as String))
|
||||||
selectedRoleId =
|
.toList();
|
||||||
availableRoles.isNotEmpty ? availableRoles.first.id : null;
|
selectedRoleId =
|
||||||
isLoadingRoles = false;
|
availableRoles.isNotEmpty ? availableRoles.first.id : null;
|
||||||
|
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) {
|
||||||
|
|||||||
@@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,105 +35,48 @@ 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
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
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),
|
),
|
||||||
Padding(
|
const SizedBox(height: 8),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
if (users.isEmpty)
|
||||||
child: Text(
|
Text(
|
||||||
snapshot.error.toString().contains('permission-denied')
|
'Aucun membre assigné.',
|
||||||
? "Vous n'avez pas la permission de voir tous les membres de l'équipe."
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
: "Erreur lors du chargement de l'équipe : ${snapshot.error}",
|
color: Colors.orange[700],
|
||||||
style: const TextStyle(color: Colors.red),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
if (users.isNotEmpty)
|
||||||
);
|
UserChipsList(
|
||||||
}
|
users: users,
|
||||||
|
showRemove: false,
|
||||||
final users = snapshot.data ?? [];
|
),
|
||||||
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),
|
|
||||||
if (users.isEmpty)
|
|
||||||
Text(
|
|
||||||
'Aucun membre assigné ou erreur de chargement.',
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
||||||
color: Colors.orange[700],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (users.isNotEmpty)
|
|
||||||
UserChipsList(
|
|
||||||
users: users,
|
|
||||||
showRemove: false,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
|
||||||
|
|
||||||
if (doc.exists) {
|
// Trouver le type correspondant
|
||||||
setState(() {
|
final eventType = eventTypes.firstWhere(
|
||||||
_eventTypeName = doc.data()?['name'] as String? ?? widget.event.eventTypeId;
|
(type) => type['id'] == widget.event.eventTypeId,
|
||||||
_isLoadingEventType = false;
|
orElse: () => <String, dynamic>{},
|
||||||
});
|
);
|
||||||
} else {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_eventTypeName = widget.event.eventTypeId;
|
_eventTypeName = eventType['name'] as String? ?? widget.event.eventTypeId;
|
||||||
_isLoadingEventType = false;
|
_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;
|
||||||
|
|||||||
@@ -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')
|
|
||||||
.doc(widget.event.id)
|
|
||||||
.snapshots(),
|
|
||||||
initialData: null,
|
|
||||||
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);
|
// Chercher l'événement mis à jour dans le provider
|
||||||
},
|
final EventModel currentEvent = eventProvider.events.firstWhere(
|
||||||
|
(e) => e.id == widget.event.id,
|
||||||
|
orElse: () => widget.event,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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,30 +23,37 @@ 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>{},
|
||||||
widget.onSelectEvent(
|
|
||||||
updatedEvent,
|
|
||||||
widget.selectedDate ?? updatedEvent.startDateTime,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await Provider.of<EventProvider>(context, listen: false)
|
if (eventData.isNotEmpty) {
|
||||||
.updateEvent(updatedEvent);
|
final updatedEvent = EventModel.fromMap(eventData, widget.event.id);
|
||||||
|
|
||||||
|
widget.onSelectEvent(
|
||||||
|
updatedEvent,
|
||||||
|
widget.selectedDate ?? updatedEvent.startDateTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
await Provider.of<EventProvider>(context, listen: false)
|
||||||
|
.updateEvent(updatedEvent);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|||||||
@@ -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,32 +16,37 @@ 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);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (mounted) {
|
||||||
SnackBar(content: Text('Erreur lors du chargement : $e')),
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
);
|
SnackBar(content: Text('Erreur lors du chargement : $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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)
|
final now = DateTime.now();
|
||||||
.get();
|
List<Map<String, dynamic>> futureEvents = [];
|
||||||
|
List<Map<String, dynamic>> pastEvents = [];
|
||||||
|
|
||||||
final now = DateTime.now();
|
for (final event in events) {
|
||||||
List<Map<String, dynamic>> futureEvents = [];
|
final eventDate = event['startDateTime'] != null
|
||||||
List<Map<String, dynamic>> pastEvents = [];
|
? DateTime.parse(event['startDateTime'] as String)
|
||||||
|
: DateTime.now();
|
||||||
|
|
||||||
for (final doc in eventsSnapshot.docs) {
|
if (eventDate.isAfter(now)) {
|
||||||
final eventData = doc.data();
|
futureEvents.add({
|
||||||
final eventDate = eventData['startDateTime']?.toDate() ?? DateTime.now();
|
'id': event['id'],
|
||||||
|
'name': event['name'],
|
||||||
if (eventDate.isAfter(now)) {
|
'startDateTime': eventDate,
|
||||||
futureEvents.add({
|
});
|
||||||
'id': doc.id,
|
} else {
|
||||||
'name': eventData['name'],
|
pastEvents.add({
|
||||||
'startDateTime': eventDate,
|
'id': event['id'],
|
||||||
});
|
'name': event['name'],
|
||||||
} else {
|
'startDateTime': eventDate,
|
||||||
pastEvents.add({
|
});
|
||||||
'id': doc.id,
|
}
|
||||||
'name': eventData['name'],
|
|
||||||
'startDateTime': eventDate,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return [...futureEvents, ...pastEvents];
|
return [...futureEvents, ...pastEvents];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deleteEventType(EventTypeModel eventType) async {
|
Future<void> _deleteEventType(EventTypeModel eventType) async {
|
||||||
@@ -198,19 +205,20 @@ 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();
|
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (mounted) {
|
||||||
const SnackBar(content: Text('Type d\'événement supprimé avec succès')),
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
);
|
const SnackBar(content: Text('Type d\'événement supprimé avec succès')),
|
||||||
|
);
|
||||||
|
}
|
||||||
_loadEventTypes();
|
_loadEventTypes();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (mounted) {
|
||||||
SnackBar(content: Text('Erreur lors de la suppression : $e')),
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
);
|
SnackBar(content: Text('Erreur lors de la suppression : $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||||
@@ -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(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
name: name,
|
||||||
const SnackBar(content: Text('Type d\'événement créé avec succès')),
|
defaultPrice: defaultPrice,
|
||||||
);
|
);
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
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,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Type d\'événement modifié avec succès')),
|
|
||||||
);
|
);
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si le code existe déjà
|
||||||
|
return !optionsData.any((opt) => opt['id'] == code);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !doc.exists;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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')),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,177 +1,240 @@
|
|||||||
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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Card(
|
if (snapshot.hasError) {
|
||||||
elevation: 2,
|
return Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
Icon(Icons.error_outline, color: Colors.red.shade700),
|
||||||
Row(
|
const SizedBox(width: 12),
|
||||||
children: [
|
Expanded(
|
||||||
const Icon(Icons.inventory_2, color: AppColors.rouge, size: 20),
|
child: Text(
|
||||||
const SizedBox(width: 8),
|
'Erreur lors du chargement des boîtes',
|
||||||
const Text(
|
style: TextStyle(color: Colors.red.shade700),
|
||||||
'Containers',
|
),
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
elevation: 2,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.inventory_2, color: AppColors.rouge, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Boîtes contenant cet équipement (${containers.length})',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
...containers.map((container) => _buildContainerCard(context, container)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(height: 24),
|
|
||||||
if (_isLoading)
|
|
||||||
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) {
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
leading: Icon(
|
|
||||||
_getTypeIcon(container.type),
|
|
||||||
color: AppColors.rouge,
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
container.id,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
subtitle: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(container.name),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
containerTypeLabel(container.type),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
trailing: const Icon(Icons.chevron_right),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pushNamed(
|
|
||||||
context,
|
|
||||||
'/container_detail',
|
|
||||||
arguments: container,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
IconData _getTypeIcon(ContainerType type) {
|
Widget _buildContainerCard(BuildContext context, ContainerModel container) {
|
||||||
switch (type) {
|
return Padding(
|
||||||
case ContainerType.flightCase:
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
return Icons.work;
|
child: Material(
|
||||||
case ContainerType.pelicase:
|
elevation: 1,
|
||||||
return Icons.work_outline;
|
borderRadius: BorderRadius.circular(12),
|
||||||
case ContainerType.bag:
|
child: InkWell(
|
||||||
return Icons.shopping_bag;
|
onTap: () {
|
||||||
case ContainerType.openCrate:
|
Navigator.push(
|
||||||
return Icons.inventory_2;
|
context,
|
||||||
case ContainerType.toolbox:
|
MaterialPageRoute(
|
||||||
return Icons.handyman;
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
|
// Informations du container
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
container.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
container.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: '${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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Icône de navigation
|
||||||
|
Icon(Icons.chevron_right, color: Colors.grey.shade400),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,191 +14,161 @@ 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>>(
|
||||||
return const SizedBox.shrink();
|
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 Card(
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
elevation: 2,
|
return Card(
|
||||||
child: Padding(
|
elevation: 2,
|
||||||
padding: const EdgeInsets.all(20),
|
child: Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.all(20),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Center(
|
||||||
children: [
|
child: CircularProgressIndicator(),
|
||||||
Row(
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return const SizedBox.shrink(); // Masquer en cas d'erreur
|
||||||
|
}
|
||||||
|
|
||||||
|
final containers = snapshot.data ?? [];
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.inventory_2, color: AppColors.rouge),
|
Row(
|
||||||
const SizedBox(width: 8),
|
children: [
|
||||||
Expanded(
|
const Icon(Icons.inventory_2, color: AppColors.rouge),
|
||||||
child: Text(
|
const SizedBox(width: 8),
|
||||||
'Containers contenant cet équipement',
|
Expanded(
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
child: Text(
|
||||||
fontWeight: FontWeight.bold,
|
'Boîtes contenant cet équipement (${containers.length})',
|
||||||
),
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
const Divider(height: 24),
|
||||||
|
_buildContainersGrid(context, containers),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(height: 24),
|
),
|
||||||
if (_isLoading)
|
);
|
||||||
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(
|
Text(
|
||||||
container.id,
|
container.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
height: 1.0,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
),
|
||||||
if (container.notes != null && container.notes!.isNotEmpty)
|
const SizedBox(height: 4),
|
||||||
Padding(
|
Text(
|
||||||
padding: const EdgeInsets.only(top: 2),
|
container.type.label,
|
||||||
child: Text(
|
style: TextStyle(
|
||||||
container.notes!,
|
fontSize: 13,
|
||||||
style: TextStyle(
|
color: Colors.grey.shade600,
|
||||||
fontSize: 10,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
height: 1.0,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Text(
|
|
||||||
container.name,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
height: 1.0,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
|
// Badges d'information
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
_buildInfoChip(
|
||||||
|
icon: Icons.inventory,
|
||||||
|
label: '${container.itemCount} équipement${container.itemCount > 1 ? 's' : ''}',
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
|
||||||
// Badges compacts
|
// Icône de navigation
|
||||||
Column(
|
Icon(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
Icons.chevron_right,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
color: AppColors.rouge,
|
||||||
children: [
|
size: 28,
|
||||||
_buildStatusBadge(_getStatusLabel(container.status), _getStatusColor(container.status)),
|
|
||||||
if (container.itemCount > 0)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 2),
|
|
||||||
child: _buildCountBadge(container.itemCount),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -207,82 +177,44 @@ class _EquipmentReferencingContainersState extends State<EquipmentReferencingCon
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
||||||
label,
|
color: color.withValues(alpha: 0.1),
|
||||||
style: TextStyle(
|
borderRadius: BorderRadius.circular(12),
|
||||||
fontSize: 9,
|
border: Border.all(
|
||||||
color: color,
|
color: color.withValues(alpha: 0.3),
|
||||||
fontWeight: FontWeight.bold,
|
width: 1,
|
||||||
height: 1.0,
|
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
),
|
||||||
maxLines: 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,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: isCompact ? 10 : 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: color.withValues(alpha: 0.9),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
445
em2rp/lib/views/widgets/equipment/parent_boxes_selector.dart
Normal file
445
em2rp/lib/views/widgets/equipment/parent_boxes_selector.dart
Normal 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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -189,10 +189,13 @@ 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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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']})',
|
||||||
|
|||||||
@@ -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);
|
|
||||||
} else if (snapshot.hasError) {
|
|
||||||
print("Error loading profile: ${snapshot.error}");
|
|
||||||
return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
|
|
||||||
} else if (snapshot.data != null && snapshot.data!.exists) {
|
|
||||||
final userData = snapshot.data!.data() as Map<String, dynamic>?;
|
|
||||||
final profilePhotoUrl = userData?['profilePhotoUrl'] as String?;
|
|
||||||
|
|
||||||
if (profilePhotoUrl != null && profilePhotoUrl.isNotEmpty) {
|
if (user == null) {
|
||||||
return CircleAvatar(
|
return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
|
||||||
radius: widget.radius,
|
}
|
||||||
backgroundImage: NetworkImage(profilePhotoUrl),
|
|
||||||
onBackgroundImageError: (e, stack) {
|
final profilePhotoUrl = user.profilePhotoUrl;
|
||||||
print("Error loading profile image: $e");
|
|
||||||
},
|
if (profilePhotoUrl.isEmpty) {
|
||||||
);
|
return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
|
return CircleAvatar(
|
||||||
|
radius: widget.radius,
|
||||||
|
backgroundImage: CachedNetworkImageProvider(profilePhotoUrl),
|
||||||
|
onBackgroundImageError: (_, __) {
|
||||||
|
// En cas d'erreur, afficher l'image par défaut
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(
|
|
||||||
radius: radius,
|
|
||||||
// Utilisation de Image.network pour l'image par défaut, avec gestion d'erreur similaire
|
|
||||||
backgroundImage: Image.network(
|
|
||||||
defaultImageUrl,
|
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
print(
|
|
||||||
"Erreur de chargement Image.network pour l'URL par défaut: $defaultImageUrl, Erreur: $error");
|
|
||||||
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(
|
return CircleAvatar(
|
||||||
radius: radius,
|
radius: radius,
|
||||||
child: FittedBox(
|
backgroundImage: defaultImageUrl != null && defaultImageUrl.isNotEmpty
|
||||||
fit: BoxFit.scaleDown,
|
? CachedNetworkImageProvider(defaultImageUrl)
|
||||||
child: Icon(Icons.account_circle, size: radius * 1.5),
|
: null,
|
||||||
),
|
child: defaultImageUrl == null || defaultImageUrl.isEmpty
|
||||||
|
? Icon(Icons.person, size: radius)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
.toList();
|
.map((data) => EventOption.fromMap(data, data['id'] as String))
|
||||||
setState(() {
|
.toList();
|
||||||
_allOptions = options;
|
setState(() {
|
||||||
_loading = false;
|
_allOptions = options;
|
||||||
});
|
_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,17 +358,22 @@ 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();
|
||||||
.toList();
|
final updatedOptions = optionsData
|
||||||
|
.map((data) => EventOption.fromMap(data, data['id'] as String))
|
||||||
|
.toList();
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentOptions = updatedOptions;
|
_currentOptions = updatedOptions;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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();
|
|
||||||
setState(() {
|
|
||||||
_allEventTypes = snapshot.docs.map((doc) => {'id': doc.id, 'name': doc['name']}).toList();
|
|
||||||
_loading = false;
|
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
|
final dataService = DataService(FirebaseFunctionsApiService());
|
||||||
|
final eventTypesData = await dataService.getEventTypes();
|
||||||
|
setState(() {
|
||||||
|
_allEventTypes = eventTypesData
|
||||||
|
.map((data) => {'id': data['id'], 'name': data['name']})
|
||||||
|
.toList();
|
||||||
|
_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,
|
||||||
});
|
});
|
||||||
Navigator.pop(context, true);
|
if (mounted) {
|
||||||
|
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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
setState(() {
|
final dataService = DataService(FirebaseFunctionsApiService());
|
||||||
availableRoles = snapshot.docs
|
final rolesData = await dataService.getRoles();
|
||||||
.map((doc) => RoleModel.fromMap(doc.data(), doc.id))
|
setState(() {
|
||||||
.toList();
|
availableRoles = rolesData
|
||||||
selectedRoleId = widget.user.role.isEmpty
|
.map((data) => RoleModel.fromMap(data, data['id'] as String))
|
||||||
? (availableRoles.isNotEmpty ? availableRoles.first.id : null)
|
.toList();
|
||||||
: widget.user.role;
|
selectedRoleId = widget.user.role.isEmpty
|
||||||
isLoadingRoles = false;
|
? (availableRoles.isNotEmpty ? availableRoles.first.id : null)
|
||||||
});
|
: widget.user.role;
|
||||||
|
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(
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user