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

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

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

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

View File

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

View File

@@ -10,7 +10,6 @@ import 'package:em2rp/services/qr_code_service.dart';
import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/views/widgets/nav/custom_app_bar.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_header_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),
// Containers parents (si applicable)
if (widget.equipment.parentBoxIds.isNotEmpty) ...[
EquipmentParentContainers(
parentBoxIds: widget.equipment.parentBoxIds,
),
const SizedBox(height: 24),
],
// Containers associés
// Containers contenant cet équipement
// Note: On utilise EquipmentReferencingContainers qui recherche dynamiquement
// les containers au lieu de se baser sur parentBoxIds qui peut être désynchronisé
EquipmentReferencingContainers(
equipmentId: widget.equipment.id,
),

View File

@@ -2,14 +2,18 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/models/container_model.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/services/equipment_service.dart';
import 'package:em2rp/services/container_equipment_service.dart';
import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
import 'package:intl/intl.dart';
import 'package:em2rp/views/equipment_form/brand_model_selector.dart';
import 'package:em2rp/utils/id_generator.dart';
import 'package:em2rp/views/widgets/equipment/parent_boxes_selector.dart';
class EquipmentFormPage extends StatefulWidget {
final EquipmentModel? equipment;
@@ -42,7 +46,7 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
DateTime? _lastMaintenanceDate;
DateTime? _nextMaintenanceDate;
List<String> _selectedParentBoxIds = [];
List<EquipmentModel> _availableBoxes = [];
List<ContainerModel> _availableBoxes = [];
bool _isLoading = false;
bool _isLoadingBoxes = true;
bool _addMultiple = false;
@@ -65,35 +69,60 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
void _populateFields() {
final equipment = widget.equipment!;
_identifierController.text = equipment.id;
_brandController.text = equipment.brand ?? '';
_selectedBrand = equipment.brand;
_modelController.text = equipment.model ?? '';
_selectedCategory = equipment.category;
_selectedStatus = equipment.status;
_purchasePriceController.text = equipment.purchasePrice?.toStringAsFixed(2) ?? '';
_rentalPriceController.text = equipment.rentalPrice?.toStringAsFixed(2) ?? '';
_totalQuantityController.text = equipment.totalQuantity?.toString() ?? '';
_criticalThresholdController.text = equipment.criticalThreshold?.toString() ?? '';
_purchaseDate = equipment.purchaseDate;
_lastMaintenanceDate = equipment.lastMaintenanceDate;
_nextMaintenanceDate = equipment.nextMaintenanceDate;
_selectedParentBoxIds = List.from(equipment.parentBoxIds);
_notesController.text = equipment.notes ?? '';
setState(() {
_identifierController.text = equipment.id;
_brandController.text = equipment.brand ?? '';
_selectedBrand = equipment.brand;
_modelController.text = equipment.model ?? '';
_selectedCategory = equipment.category;
_selectedStatus = equipment.status;
_purchasePriceController.text = equipment.purchasePrice?.toStringAsFixed(2) ?? '';
_rentalPriceController.text = equipment.rentalPrice?.toStringAsFixed(2) ?? '';
_totalQuantityController.text = equipment.totalQuantity?.toString() ?? '';
_criticalThresholdController.text = equipment.criticalThreshold?.toString() ?? '';
_purchaseDate = equipment.purchaseDate;
_lastMaintenanceDate = equipment.lastMaintenanceDate;
_nextMaintenanceDate = equipment.nextMaintenanceDate;
_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) {
_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 {
try {
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(() {
_availableBoxes = boxes;
_isLoadingBoxes = false;
});
} catch (e) {
print('[EquipmentForm] Error loading boxes: $e');
setState(() {
_isLoadingBoxes = false;
});
@@ -390,11 +419,14 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
],
// Boîtes parentes
const Divider(),
const Text('Boîtes parentes', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
_isLoadingBoxes
? const Center(child: CircularProgressIndicator())
? const Card(
child: Padding(
padding: EdgeInsets.all(32.0),
child: Center(child: CircularProgressIndicator()),
),
)
: _buildParentBoxesSelector(),
const SizedBox(height: 16),
@@ -449,35 +481,14 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
}
Widget _buildParentBoxesSelector() {
if (_availableBoxes.isEmpty) {
return const Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Text('Aucune boîte disponible'),
),
);
}
return Card(
child: Column(
children: _availableBoxes.map((box) {
final isSelected = _selectedParentBoxIds.contains(box.id);
return CheckboxListTile(
title: Text(box.name),
subtitle: box.model != null ? Text('Modèle: {box.model}') : null,
value: isSelected,
onChanged: (bool? value) {
setState(() {
if (value == true) {
_selectedParentBoxIds.add(box.id);
} else {
_selectedParentBoxIds.remove(box.id);
}
});
},
);
}).toList(),
),
return ParentBoxesSelector(
availableBoxes: _availableBoxes,
selectedBoxIds: _selectedParentBoxIds,
onSelectionChanged: (newSelection) {
setState(() {
_selectedParentBoxIds = newSelection;
});
},
);
}
@@ -625,19 +636,66 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
purchaseDate: _purchaseDate,
lastMaintenanceDate: _lastMaintenanceDate,
nextMaintenanceDate: _nextMaintenanceDate,
parentBoxIds: _selectedParentBoxIds,
parentBoxIds: [], // On ne stocke plus les parentBoxIds dans l'équipement
notes: _notesController.text,
createdAt: isEditing ? (widget.equipment?.createdAt ?? now) : now,
updatedAt: now,
availableQuantity: availableQuantity,
);
if (isEditing) {
await equipmentProvider.updateEquipment(
equipment.id,
equipment.toMap(),
);
await equipmentProvider.updateEquipment(equipment);
// 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 {
await equipmentProvider.addEquipment(equipment);
// Pour un nouvel équipement, ajouter à tous les containers sélectionnés
for (final boxId in _selectedParentBoxIds) {
try {
final containerProvider = Provider.of<ContainerProvider>(context, listen: false);
await containerProvider.addEquipmentToContainer(
containerId: boxId,
equipmentId: equipment.id,
);
print('[EquipmentForm] Added new equipment ${equipment.id} to container $boxId');
} catch (e) {
print('[EquipmentForm] Error adding new equipment to container $boxId: $e');
}
}
}
}

View File

@@ -29,6 +29,17 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
EquipmentCategory? _selectedCategory;
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
void dispose() {
_searchController.dispose();
@@ -420,16 +431,44 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
Widget _buildEquipmentList() {
return Consumer<EquipmentProvider>(
builder: (context, provider, child) {
return ManagementList<EquipmentModel>(
stream: provider.equipmentStream,
cachedItems: _cachedEquipment,
emptyMessage: 'Aucun équipement trouvé',
emptyIcon: Icons.inventory_2_outlined,
onDataReceived: (items) {
_cachedEquipment = items;
},
itemBuilder: (equipment) {
return _buildEquipmentCard(equipment);
print('[EquipmentManagementPage] Building list - isLoading: ${provider.isLoading}, equipment count: ${provider.equipment.length}');
if (provider.isLoading && _cachedEquipment == null) {
print('[EquipmentManagementPage] Showing loading indicator');
return const Center(child: CircularProgressIndicator());
}
final equipments = provider.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(),
};
await context.read<EquipmentProvider>().updateEquipment(
equipment.id,
updatedData,
);
final updatedEquipment = equipment.copyWith(
availableQuantity: newAvailable,
totalQuantity: newTotal,
updatedAt: DateTime.now(),
);
await context.read<EquipmentProvider>().updateEquipment(updatedEquipment);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(

View File

@@ -6,8 +6,10 @@ 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/container_provider.dart';
import 'package:em2rp/services/event_preparation_service.dart';
import 'package:em2rp/services/event_preparation_service_extended.dart';
import 'package:em2rp/providers/event_provider.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/missing_equipment_dialog.dart';
import 'package:em2rp/utils/colors.dart';
@@ -34,9 +36,8 @@ class EventPreparationPage extends StatefulWidget {
}
class _EventPreparationPageState extends State<EventPreparationPage> with SingleTickerProviderStateMixin {
final EventPreparationService _preparationService = EventPreparationService();
final EventPreparationServiceExtended _extendedService = EventPreparationServiceExtended();
late AnimationController _animationController;
late final DataService _dataService;
Map<String, EquipmentModel> _equipmentCache = {};
Map<String, ContainerModel> _containerCache = {};
@@ -89,6 +90,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
void initState() {
super.initState();
_currentEvent = widget.initialEvent;
_dataService = DataService(FirebaseFunctionsApiService());
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
@@ -131,24 +133,6 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
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 {
setState(() => _isLoading = true);
@@ -293,11 +277,15 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
break;
}
// Sauvegarder dans Firestore
await FirebaseFirestore.instance
.collection('events')
.doc(_currentEvent.id)
.update(updateData);
// Sauvegarder dans Firestore via l'API
await _dataService.updateEventEquipment(
eventId: _currentEvent.id,
assignedEquipment: updatedEquipment.map((e) => e.toMap()).toList(),
preparationStatus: updateData['preparationStatus'],
loadingStatus: updateData['loadingStatus'],
unloadingStatus: updateData['unloadingStatus'],
returnStatus: updateData['returnStatus'],
);
// Mettre à jour les statuts des équipements si nécessaire
if (_currentStep == PreparationStep.preparation ||
@@ -305,6 +293,14 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
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);
_animationController.forward();
@@ -338,52 +334,37 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
Future<void> _updateEquipmentStatuses(List<EventEquipment> equipment) async {
for (var eq in equipment) {
try {
final doc = await FirebaseFirestore.instance
.collection('equipments')
.doc(eq.equipmentId)
.get();
final equipmentData = _equipmentCache[eq.equipmentId];
if (equipmentData == null) continue;
if (doc.exists) {
final equipmentData = EquipmentModel.fromMap(
doc.data() as Map<String, dynamic>,
doc.id,
// Déterminer le nouveau statut
EquipmentStatus newStatus;
if (eq.isReturned) {
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
EquipmentStatus newStatus;
if (eq.isReturned) {
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 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()),
});
}
// Gérer les stocks pour les consommables
if (equipmentData.hasQuantity && eq.isReturned && eq.returnedQuantity != null) {
final currentAvailable = equipmentData.availableQuantity ?? 0;
await _dataService.updateEquipmentStatusOnly(
equipmentId: eq.equipmentId,
availableQuantity: currentAvailable + eq.returnedQuantity!,
);
}
} catch (e) {
print('Error updating equipment status for ${eq.equipmentId}: $e');
// Erreur silencieuse pour ne pas bloquer le processus
}
}
}

View File

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

View File

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

View File

@@ -1,19 +1,21 @@
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/event_model.dart';
import 'package:em2rp/providers/event_provider.dart';
import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart';
class EventDetailsEquipe extends StatelessWidget {
final List workforce;
final EventModel event;
const EventDetailsEquipe({
super.key,
required this.workforce,
required this.event,
});
@override
Widget build(BuildContext context) {
if (workforce.isEmpty) {
if (event.workforce.isEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -33,105 +35,48 @@ class EventDetailsEquipe extends StatelessWidget {
);
}
return FutureBuilder<List<UserModel>>(
future: _fetchUsers(),
builder: (context, snapshot) {
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()),
),
],
);
}
// Récupérer les utilisateurs depuis le cache du provider
final eventProvider = Provider.of<EventProvider>(context, listen: false);
final workforceUsers = eventProvider.getWorkforceUsers(event);
if (snapshot.hasError) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Equipe',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.black,
fontWeight: FontWeight.bold,
),
// Convertir en UserModel
final users = workforceUsers.map((userData) {
return UserModel(
uid: userData['uid'] ?? '',
firstName: userData['firstName'] ?? '',
lastName: userData['lastName'] ?? '',
email: userData['email'] ?? '',
phoneNumber: userData['phoneNumber'] ?? '',
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(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Text(
snapshot.error.toString().contains('permission-denied')
? "Vous n'avez pas la permission de voir tous les membres de l'équipe."
: "Erreur lors du chargement de l'équipe : ${snapshot.error}",
style: const TextStyle(color: Colors.red),
),
const SizedBox(height: 8),
if (users.isEmpty)
Text(
'Aucun membre assigné.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.orange[700],
),
),
],
);
}
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,
),
],
);
},
),
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;
}
}

View File

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

View File

@@ -1,6 +1,7 @@
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/providers/event_provider.dart';
import 'package:em2rp/views/event_preparation_page.dart';
import 'package:em2rp/utils/colors.dart';
@@ -20,30 +21,19 @@ class EventPreparationButtons extends StatefulWidget {
class _EventPreparationButtonsState extends State<EventPreparationButtons> {
@override
Widget build(BuildContext context) {
// Écouter les changements de l'événement en temps réel
return StreamBuilder<DocumentSnapshot>(
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;
}
// Utiliser le provider pour récupérer l'événement à jour
final eventProvider = context.watch<EventProvider>();
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) {
// Vérifier s'il y a du matériel assigné
final hasMaterial = event.assignedEquipment.isNotEmpty || event.assignedContainers.isNotEmpty;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,177 +1,240 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:em2rp/utils/colors.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
class EquipmentParentContainers extends StatefulWidget {
final List<String> parentBoxIds;
/// Widget pour afficher les boîtes contenant un équipement
/// Utilise le nouveau système : interroge Firestore via Cloud Function
class EquipmentParentContainers extends StatelessWidget {
final String equipmentId;
const EquipmentParentContainers({
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
Widget build(BuildContext context) {
if (widget.parentBoxIds.isEmpty) {
return const SizedBox.shrink();
}
return FutureBuilder<List<ContainerModel>>(
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(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.inventory_2, color: AppColors.rouge, size: 20),
const SizedBox(width: 8),
const Text(
'Containers',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
if (snapshot.hasError) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Icon(Icons.error_outline, color: Colors.red.shade700),
const SizedBox(width: 12),
Expanded(
child: Text(
'Erreur lors du chargement des boîtes',
style: TextStyle(color: Colors.red.shade700),
),
),
],
),
),
);
}
final containers = snapshot.data ?? [];
if (containers.isEmpty) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Icon(Icons.info_outline, color: Colors.grey.shade600),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Cet équipement n\'est dans aucune boîte',
style: TextStyle(color: Colors.grey),
),
),
],
),
),
);
}
return Card(
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) {
switch (type) {
case ContainerType.flightCase:
return Icons.work;
case ContainerType.pelicase:
return Icons.work_outline;
case ContainerType.bag:
return Icons.shopping_bag;
case ContainerType.openCrate:
return Icons.inventory_2;
case ContainerType.toolbox:
return Icons.handyman;
}
Widget _buildContainerCard(BuildContext context, ContainerModel container) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Material(
elevation: 1,
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ContainerDetailPage(container: container),
),
);
},
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
// Icône du type de container
CircleAvatar(
backgroundColor: AppColors.rouge.withValues(alpha: 0.1),
radius: 24,
child: container.type.getIconForAvatar(
size: 24,
color: AppColors.rouge,
),
),
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),
),
),
],
),
);
}
}

View File

@@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import 'package:em2rp/models/container_model.dart';
import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/services/container_service.dart';
import 'package:em2rp/services/container_equipment_service.dart';
import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/views/container_detail_page.dart';
/// 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;
const EquipmentReferencingContainers({
@@ -14,191 +14,161 @@ class EquipmentReferencingContainers extends StatefulWidget {
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
Widget build(BuildContext context) {
if (_referencingContainers.isEmpty && !_isLoading) {
return const SizedBox.shrink();
}
return FutureBuilder<List<ContainerModel>>(
future: containerEquipmentService.getContainersByEquipment(equipmentId),
builder: (context, snapshot) {
// Ne rien afficher si vide et pas en chargement
if (snapshot.connectionState == ConnectionState.done &&
(!snapshot.hasData || snapshot.data!.isEmpty)) {
return const SizedBox.shrink();
}
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
if (snapshot.connectionState == ConnectionState.waiting) {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(20),
child: Center(
child: CircularProgressIndicator(),
),
),
);
}
if (snapshot.hasError) {
return const SizedBox.shrink(); // Masquer en cas d'erreur
}
final containers = snapshot.data ?? [];
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.inventory_2, color: AppColors.rouge),
const SizedBox(width: 8),
Expanded(
child: Text(
'Containers contenant cet équipement',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
Row(
children: [
const Icon(Icons.inventory_2, color: AppColors.rouge),
const SizedBox(width: 8),
Expanded(
child: Text(
'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(
margin: const EdgeInsets.only(bottom: 12),
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.grey.shade200, width: 1),
),
child: InkWell(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ContainerDetailPage(container: container),
builder: (_) => ContainerDetailPage(container: container),
),
);
},
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Icône du type de container
container.type.getIcon(size: 28, color: AppColors.rouge),
const SizedBox(width: 10),
// Infos textuelles
CircleAvatar(
backgroundColor: AppColors.rouge.withValues(alpha: 0.1),
radius: 28,
child: container.type.getIconForAvatar(
size: 28,
color: AppColors.rouge,
),
),
const SizedBox(width: 16),
// Informations du container
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
container.id,
container.name,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
height: 1.0,
fontSize: 16,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
if (container.notes != null && container.notes!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
container.notes!,
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
height: 1.0,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
)
else
Text(
container.name,
style: TextStyle(
fontSize: 11,
color: Colors.grey[600],
height: 1.0,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
const SizedBox(height: 4),
Text(
container.type.label,
style: TextStyle(
fontSize: 13,
color: Colors.grey.shade600,
),
),
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
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
_buildStatusBadge(_getStatusLabel(container.status), _getStatusColor(container.status)),
if (container.itemCount > 0)
Padding(
padding: const EdgeInsets.only(top: 2),
child: _buildCountBadge(container.itemCount),
),
],
// Icône de navigation
Icon(
Icons.chevron_right,
color: AppColors.rouge,
size: 28,
),
],
),
@@ -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(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withValues(alpha: 0.4), width: 0.5),
padding: EdgeInsets.symmetric(
horizontal: isCompact ? 6 : 8,
vertical: isCompact ? 2 : 4,
),
child: Text(
label,
style: TextStyle(
fontSize: 9,
color: color,
fontWeight: FontWeight.bold,
height: 1.0,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: color.withValues(alpha: 0.3),
width: 1,
),
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;
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:intl/intl.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 {
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 {
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) {
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) {
final doc = await FirebaseFirestore.instance
.collection('options')
.doc(optionData['id'])
.get();
final apiData = optionsMap[optionData['id']];
if (doc.exists) {
final firestoreData = doc.data()!;
// Combiner les données Firestore avec le prix choisi
if (apiData != null) {
// Combiner les données API avec le prix choisi
enrichedOptions.add({
'id': optionData['id'],
'code': firestoreData['code'] ?? optionData['id'], // Récupérer le code depuis Firestore
'name': firestoreData['name'], // Récupéré depuis Firestore
'details': firestoreData['details'] ?? '', // Récupéré depuis Firestore
'code': apiData['code'] ?? optionData['id'],
'name': apiData['name'],
'details': apiData['details'] ?? '',
'price': optionData['price'], // Prix choisi par l'utilisateur
'quantity': optionData['quantity'] ?? 1, // Quantité
'isQuantitative': firestoreData['isQuantitative'] ?? false,
'valMin': firestoreData['valMin'],
'valMax': firestoreData['valMax'],
'isQuantitative': apiData['isQuantitative'] ?? false,
'valMin': apiData['valMin'],
'valMax': apiData['valMax'],
});
} 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({
'id': optionData['id'],
'name': 'Option supprimée (ID: ${optionData['id']})',

View File

@@ -1,16 +1,20 @@
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 {
final String? userId;
final double radius;
final String? defaultImageUrl;
final String? profilePhotoUrl; // URL directe de la photo (optionnel)
const ProfilePictureWidget({
super.key,
required this.userId,
this.radius = 25,
this.userId,
this.radius = 20,
this.defaultImageUrl,
this.profilePhotoUrl, // Si fourni, utilisé directement sans appeler UsersProvider
});
@override
@@ -18,110 +22,56 @@ class ProfilePictureWidget extends StatefulWidget {
}
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
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) {
return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
}
return FutureBuilder<DocumentSnapshot?>(
future: _userFuture,
builder: (context, snapshot) {
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?;
// Utiliser le provider pour récupérer l'utilisateur
final usersProvider = context.watch<UsersProvider>();
final user = usersProvider.getUserById(widget.userId!);
if (profilePhotoUrl != null && profilePhotoUrl.isNotEmpty) {
return CircleAvatar(
radius: widget.radius,
backgroundImage: NetworkImage(profilePhotoUrl),
onBackgroundImageError: (e, stack) {
print("Error loading profile image: $e");
},
);
}
}
return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
if (user == null) {
return _buildDefaultAvatar(widget.radius, widget.defaultImageUrl);
}
final profilePhotoUrl = user.profilePhotoUrl;
if (profilePhotoUrl.isEmpty) {
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) {
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(
radius: radius,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Icon(Icons.account_circle, size: radius * 1.5),
),
backgroundImage: defaultImageUrl != null && defaultImageUrl.isNotEmpty
? CachedNetworkImageProvider(defaultImageUrl)
: null,
child: defaultImageUrl == null || defaultImageUrl.isEmpty
? Icon(Icons.person, size: radius)
: null,
);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.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 {
final List<Map<String, dynamic>> selectedOptions;
@@ -42,15 +43,20 @@ class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
Future<void> _fetchOptions() async {
setState(() => _loading = true);
final snapshot =
await FirebaseFirestore.instance.collection('options').get();
final options = snapshot.docs
.map((doc) => EventOption.fromMap(doc.data(), doc.id))
.toList();
setState(() {
_allOptions = options;
_loading = false;
});
try {
final dataService = DataService(FirebaseFunctionsApiService());
final optionsData = await dataService.getOptions();
final options = optionsData
.map((data) => EventOption.fromMap(data, data['id'] as String))
.toList();
setState(() {
_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
@@ -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 {
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) {
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) {
final doc = await FirebaseFirestore.instance
.collection('options')
.doc(optionData['id'])
.get();
final firestoreData = optionsMap[optionData['id']];
if (doc.exists) {
final firestoreData = doc.data()!;
if (firestoreData != null) {
enrichedOptions.add({
'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
? '${firestoreData['code']} - ${firestoreData['name']}'
: firestoreData['name'], // Affichage avec code
: firestoreData['name'],
'details': firestoreData['details'] ?? '',
'price': optionData['price'],
'quantity': optionData['quantity'] ?? 1,
@@ -347,17 +358,22 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> {
}
Future<void> _reloadOptions() async {
final snapshot = await FirebaseFirestore.instance.collection('options').get();
final updatedOptions = snapshot.docs
.map((doc) => EventOption.fromMap(doc.data(), doc.id))
.toList();
try {
final dataService = DataService(FirebaseFunctionsApiService());
final optionsData = await dataService.getOptions();
final updatedOptions = optionsData
.map((data) => EventOption.fromMap(data, data['id'] as String))
.toList();
setState(() {
_currentOptions = updatedOptions;
});
setState(() {
_currentOptions = updatedOptions;
});
// Appeler le callback pour mettre à jour aussi le parent
widget.onOptionsUpdated?.call(updatedOptions);
// Appeler le callback pour mettre à jour aussi le parent
widget.onOptionsUpdated?.call(updatedOptions);
} catch (e) {
// Erreur silencieuse
}
}
@override
@@ -557,23 +573,34 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
bool _loading = true;
Future<bool> _isCodeUnique(String code) async {
// Vérifier si le document avec ce code existe déjà
final doc = await FirebaseFirestore.instance
.collection('options')
.doc(code)
.get();
return !doc.exists;
try {
// Charger toutes les options via l'API
final dataService = DataService(FirebaseFunctionsApiService());
final optionsData = await dataService.getOptions();
// Vérifier si le code existe déjà
return !optionsData.any((opt) => opt['id'] == code);
} catch (e) {
return false;
}
}
Future<void> _fetchEventTypes() async {
setState(() {
_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;
_loading = true;
});
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
@@ -741,8 +768,9 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
return;
}
try {
// Utiliser le code comme identifiant du document
await FirebaseFirestore.instance.collection('options').doc(code).set({
// Créer via l'API
final dataService = DataService(FirebaseFunctionsApiService());
await dataService.createOption(code, {
'code': code,
'name': name,
'details': _detailsController.text.trim(),
@@ -751,7 +779,9 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
'eventTypes': _selectedTypes,
'isQuantitative': _isQuantitative,
});
Navigator.pop(context, true);
if (mounted) {
Navigator.pop(context, true);
}
} catch (e) {
setState(() => _error = 'Erreur lors de la création : $e');
}

View File

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

View File

@@ -200,7 +200,10 @@ class UserChipsList extends StatelessWidget {
children: users
.map((user) => Chip(
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}',
style: const TextStyle(fontSize: 16)),
labelPadding: const EdgeInsets.symmetric(horizontal: 8),