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