Cette mise à jour majeure introduit une fonctionnalité de scan et de saisie manuelle de codes QR directement depuis la page de préparation d'un événement. Ce système accélère et fiabilise le processus de validation des équipements et des containers pour chaque étape (préparation, chargement, etc.), tout en ajoutant des retours sonores, haptiques et visuels pour une expérience utilisateur améliorée.
**Fonctionnalités et améliorations principales :**
- **Scan et saisie manuelle en préparation d'événement :**
- Ajout d'un champ de "Saisie manuelle" et d'un bouton "Scanner QR Code" sur la page de préparation (`EventPreparationPage`).
- Le scanner peut fonctionner en mode "multi-scan", permettant de valider plusieurs éléments à la suite sans fermer la caméra.
- Le système gère à la fois les équipements individuels et les containers (qui valident automatiquement tout leur contenu).
- **Logique de traitement intelligente (`QRCodeProcessingService`) :**
- Un nouveau service centralise la logique de traitement des codes.
- Pour les équipements quantitatifs, chaque scan incrémente la quantité jusqu'à atteindre la cible requise pour l'étape en cours.
- Pour les équipements non quantitatifs, le premier scan valide l'élément.
- Les scans multiples d'un élément déjà validé ou dont la quantité est atteinte génèrent une erreur.
- **Ajout dynamique d'équipements :**
- Si un code scanné n'est pas assigné à l'événement, une boîte de dialogue propose de rechercher l'équipement ou le container dans la base de données et de l'ajouter à l'événement en cours.
- **Feedbacks utilisateur :**
- Création d'un `AudioFeedbackService` pour fournir des retours sonores (succès/erreur) et haptiques (vibration) lors de chaque scan.
- Des `Snackbars` claires (vertes pour succès, orange pour erreur) informent l'utilisateur du résultat de chaque action.
- **Optimisation du chargement des données :**
- Nouvel endpoint backend `getEventWithDetails` qui charge un événement et toutes ses dépendances (équipements, containers et leurs enfants) en un seul appel, optimisant drastiquement les temps de chargement des pages de préparation et de modification d'événement.
- Le frontend (`EventPreparationPage`, `EventAssignedEquipmentSection`) utilise ce nouvel endpoint, éliminant les chargements multiples et fiabilisant l'affichage des données.
**Refactorisation et corrections :**
- **Structure du code :**
- La logique de traitement des codes est extraite dans le `QRCodeProcessingService`.
- Création de widgets dédiés (`CodeNotFoundDialog`, `AddEquipmentToEventDialog`) pour gérer les nouveaux flux utilisateurs.
- **Fiabilisation de l'état :**
- Mise à jour optimiste de l'UI lors du changement de statut d'un événement (`EventStatusButton`) pour une meilleure réactivité.
- Correction d'un bug dans la suppression d'un container d'un événement, qui pouvait retirer des équipements partagés avec d'autres containers.
- Correction d'un bug lors de l'ajout d'un container à un événement, qui n'ajoutait pas automatiquement ses équipements enfants.
- **Optimisations des performances UI :**
- Amélioration de la fluidité du défilement infini sur la page de gestion des équipements grâce à `RepaintBoundary` et à une gestion optimisée du chargement.
**Déploiement et version :**
- **Scripts de déploiement :** Ajout d'un script PowerShell (`deploy_hosting.ps1`) et amélioration du script Node.js pour automatiser et fiabiliser les déploiements sur Firebase Hosting.
- **Configuration CORS :** Les en-têtes CORS sont désormais configurés pour `version.json`, assurant le bon fonctionnement du mécanisme de mise à jour de l'application.
- **Version de l'application :** Incrémentée à `1.0.6`.
502 lines
18 KiB
Dart
502 lines
18 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:em2rp/models/event_model.dart';
|
|
import 'package:em2rp/models/event_type_model.dart';
|
|
import 'package:em2rp/models/user_model.dart';
|
|
import 'package:em2rp/services/event_form_service.dart';
|
|
import 'package:em2rp/services/data_service.dart';
|
|
import 'package:em2rp/services/api_service.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:em2rp/providers/event_provider.dart';
|
|
import 'package:em2rp/providers/local_user_provider.dart';
|
|
|
|
class EventFormController extends ChangeNotifier {
|
|
// Controllers
|
|
final TextEditingController nameController = TextEditingController();
|
|
final TextEditingController descriptionController = TextEditingController();
|
|
final TextEditingController basePriceController = TextEditingController();
|
|
final TextEditingController installationController = TextEditingController();
|
|
final TextEditingController disassemblyController = TextEditingController();
|
|
final TextEditingController addressController = TextEditingController();
|
|
final TextEditingController jaugeController = TextEditingController();
|
|
final TextEditingController contactEmailController = TextEditingController();
|
|
final TextEditingController contactPhoneController = TextEditingController();
|
|
|
|
// State variables
|
|
DateTime? _startDateTime;
|
|
DateTime? _endDateTime;
|
|
bool _isLoading = false;
|
|
String? _error;
|
|
String? _success;
|
|
String? _selectedEventTypeId;
|
|
List<EventTypeModel> _eventTypes = [];
|
|
bool _isLoadingEventTypes = true;
|
|
List<String> _selectedUserIds = [];
|
|
List<UserModel> _allUsers = [];
|
|
bool _isLoadingUsers = true;
|
|
List<Map<String, String>> _uploadedFiles = [];
|
|
List<Map<String, dynamic>> _selectedOptions = [];
|
|
bool _formChanged = false;
|
|
EventStatus _selectedStatus = EventStatus.waitingForApproval;
|
|
List<EventEquipment> _assignedEquipment = [];
|
|
List<String> _assignedContainers = [];
|
|
|
|
// Getters
|
|
DateTime? get startDateTime => _startDateTime;
|
|
DateTime? get endDateTime => _endDateTime;
|
|
bool get isLoading => _isLoading;
|
|
String? get error => _error;
|
|
String? get success => _success;
|
|
String? get selectedEventTypeId => _selectedEventTypeId;
|
|
List<EventTypeModel> get eventTypes => _eventTypes;
|
|
bool get isLoadingEventTypes => _isLoadingEventTypes;
|
|
List<String> get selectedUserIds => _selectedUserIds;
|
|
List<UserModel> get allUsers => _allUsers;
|
|
bool get isLoadingUsers => _isLoadingUsers;
|
|
List<Map<String, String>> get uploadedFiles => _uploadedFiles;
|
|
List<Map<String, dynamic>> get selectedOptions => _selectedOptions;
|
|
List<EventEquipment> get assignedEquipment => _assignedEquipment;
|
|
List<String> get assignedContainers => _assignedContainers;
|
|
bool get formChanged => _formChanged;
|
|
EventStatus get selectedStatus => _selectedStatus;
|
|
|
|
EventFormController() {
|
|
_setupListeners();
|
|
}
|
|
|
|
void _setupListeners() {
|
|
nameController.addListener(_onAnyFieldChanged);
|
|
basePriceController.addListener(_onAnyFieldChanged);
|
|
installationController.addListener(_onAnyFieldChanged);
|
|
disassemblyController.addListener(_onAnyFieldChanged);
|
|
addressController.addListener(_onAnyFieldChanged);
|
|
descriptionController.addListener(_onAnyFieldChanged);
|
|
jaugeController.addListener(_onAnyFieldChanged);
|
|
contactEmailController.addListener(_onAnyFieldChanged);
|
|
contactPhoneController.addListener(_onAnyFieldChanged);
|
|
}
|
|
|
|
void _onAnyFieldChanged() {
|
|
if (!_formChanged) {
|
|
_formChanged = true;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> initialize({EventModel? existingEvent, DateTime? selectedDate}) async {
|
|
await Future.wait([
|
|
_fetchUsers(),
|
|
_fetchEventTypes(),
|
|
]);
|
|
|
|
if (existingEvent != null) {
|
|
// 🔧 FIX: Recharger l'événement avec tous les détails (équipements + containers avec enfants)
|
|
try {
|
|
final dataService = DataService(FirebaseFunctionsApiService());
|
|
final result = await dataService.getEventWithDetails(existingEvent.id);
|
|
final eventData = result['event'] as Map<String, dynamic>;
|
|
|
|
// Reconstruire l'événement avec les données complètes
|
|
final completeEvent = EventModel.fromMap(eventData, eventData['id'] as String);
|
|
_populateFromEvent(completeEvent);
|
|
} catch (e) {
|
|
// Si erreur, utiliser l'événement existant (fallback)
|
|
print('[EventFormController] Error loading event with details, using existing: $e');
|
|
_populateFromEvent(existingEvent);
|
|
}
|
|
} else {
|
|
_selectedStatus = EventStatus.waitingForApproval;
|
|
|
|
// Préremplir les dates si une date est sélectionnée dans le calendrier
|
|
if (selectedDate != null) {
|
|
// Date de début : selectedDate à 20h00
|
|
_startDateTime = DateTime(
|
|
selectedDate.year,
|
|
selectedDate.month,
|
|
selectedDate.day,
|
|
20,
|
|
0,
|
|
);
|
|
// Date de fin : selectedDate + 4 heures
|
|
_endDateTime = _startDateTime!.add(const Duration(hours: 4));
|
|
}
|
|
}
|
|
notifyListeners();
|
|
}
|
|
|
|
void _populateFromEvent(EventModel event) {
|
|
nameController.text = event.name;
|
|
descriptionController.text = event.description;
|
|
basePriceController.text = event.basePrice.toStringAsFixed(2);
|
|
installationController.text = event.installationTime.toString();
|
|
disassemblyController.text = event.disassemblyTime.toString();
|
|
addressController.text = event.address;
|
|
jaugeController.text = event.jauge?.toString() ?? '';
|
|
contactEmailController.text = event.contactEmail ?? '';
|
|
contactPhoneController.text = event.contactPhone ?? '';
|
|
_startDateTime = event.startDateTime;
|
|
_endDateTime = event.endDateTime;
|
|
_assignedEquipment = List<EventEquipment>.from(event.assignedEquipment);
|
|
_assignedContainers = List<String>.from(event.assignedContainers);
|
|
_selectedEventTypeId = event.eventTypeId.isNotEmpty ? event.eventTypeId : null;
|
|
|
|
// Gérer workforce qui peut contenir String ou DocumentReference
|
|
_selectedUserIds = event.workforce.map((ref) {
|
|
if (ref is String) return ref;
|
|
if (ref is DocumentReference) return ref.id;
|
|
return '';
|
|
}).where((id) => id.isNotEmpty).toList();
|
|
|
|
_uploadedFiles = List<Map<String, String>>.from(event.documents);
|
|
_selectedOptions = List<Map<String, dynamic>>.from(event.options);
|
|
_selectedStatus = event.status;
|
|
}
|
|
|
|
Future<void> _fetchUsers() async {
|
|
try {
|
|
_allUsers = await EventFormService.fetchUsers();
|
|
_isLoadingUsers = false;
|
|
} catch (e) {
|
|
_error = e.toString();
|
|
_isLoadingUsers = false;
|
|
}
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> _fetchEventTypes() async {
|
|
try {
|
|
_eventTypes = await EventFormService.fetchEventTypes();
|
|
_isLoadingEventTypes = false;
|
|
} catch (e) {
|
|
_error = e.toString();
|
|
_isLoadingEventTypes = false;
|
|
}
|
|
notifyListeners();
|
|
}
|
|
|
|
void setStartDateTime(DateTime? dateTime) {
|
|
_startDateTime = dateTime;
|
|
if (_endDateTime != null &&
|
|
dateTime != null &&
|
|
(_endDateTime!.isBefore(dateTime) || _endDateTime!.isAtSameMomentAs(dateTime))) {
|
|
_endDateTime = null;
|
|
}
|
|
_onAnyFieldChanged();
|
|
notifyListeners();
|
|
}
|
|
|
|
void setEndDateTime(DateTime? dateTime) {
|
|
_endDateTime = dateTime;
|
|
_onAnyFieldChanged();
|
|
notifyListeners();
|
|
}
|
|
|
|
void onEventTypeChanged(String? newTypeId, BuildContext context) {
|
|
if (newTypeId == _selectedEventTypeId) return;
|
|
|
|
final oldEventTypeIndex = _selectedEventTypeId != null
|
|
? _eventTypes.indexWhere((et) => et.id == _selectedEventTypeId)
|
|
: -1;
|
|
final EventTypeModel? oldEventType = oldEventTypeIndex != -1 ? _eventTypes[oldEventTypeIndex] : null;
|
|
|
|
_selectedEventTypeId = newTypeId;
|
|
|
|
if (newTypeId != null) {
|
|
final selectedType = _eventTypes.firstWhere((et) => et.id == newTypeId);
|
|
|
|
// Utiliser le prix par défaut du type d'événement (prix TTC stocké dans basePrice)
|
|
final defaultPriceTTC = selectedType.defaultPrice;
|
|
final currentPrice = double.tryParse(basePriceController.text.replaceAll(',', '.'));
|
|
final oldDefaultPrice = oldEventType?.defaultPrice;
|
|
|
|
// Mettre à jour le prix TTC si le champ est vide ou si c'était l'ancien prix par défaut
|
|
if (basePriceController.text.isEmpty ||
|
|
(currentPrice != null && oldDefaultPrice != null && currentPrice == oldDefaultPrice)) {
|
|
basePriceController.text = defaultPriceTTC.toStringAsFixed(2);
|
|
}
|
|
|
|
// Filtrer les options qui ne sont plus compatibles avec le nouveau type
|
|
final before = _selectedOptions.length;
|
|
_selectedOptions.removeWhere((opt) {
|
|
// Vérifier si cette option est compatible avec le type d'événement sélectionné
|
|
final optionEventTypes = opt['eventTypes'] as List<dynamic>? ?? [];
|
|
return !optionEventTypes.contains(selectedType.id);
|
|
});
|
|
|
|
if (_selectedOptions.length < before) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'Certaines options ont été retirées car non compatibles avec "${selectedType.name}".')),
|
|
);
|
|
}
|
|
} else {
|
|
_selectedOptions.clear();
|
|
}
|
|
|
|
_onAnyFieldChanged();
|
|
notifyListeners();
|
|
}
|
|
|
|
void setSelectedUserIds(List<String> userIds) {
|
|
_selectedUserIds = userIds;
|
|
_onAnyFieldChanged();
|
|
notifyListeners();
|
|
}
|
|
|
|
void setUploadedFiles(List<Map<String, String>> files) {
|
|
_uploadedFiles = files;
|
|
_onAnyFieldChanged();
|
|
notifyListeners();
|
|
}
|
|
|
|
void setSelectedOptions(List<Map<String, dynamic>> options) {
|
|
_selectedOptions = options;
|
|
_onAnyFieldChanged();
|
|
notifyListeners();
|
|
}
|
|
|
|
void setAssignedEquipment(List<EventEquipment> equipment, List<String> containers) {
|
|
_assignedEquipment = equipment;
|
|
_assignedContainers = containers;
|
|
_onAnyFieldChanged();
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> pickAndUploadFiles() async {
|
|
final result = await FilePicker.platform.pickFiles(allowMultiple: true, withData: true);
|
|
if (result != null && result.files.isNotEmpty) {
|
|
_isLoading = true;
|
|
_error = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final files = await EventFormService.uploadFiles(result.files);
|
|
_uploadedFiles.addAll(files);
|
|
_onAnyFieldChanged();
|
|
} catch (e) {
|
|
_error = 'Erreur lors de l\'upload : $e';
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool validateForm() {
|
|
return nameController.text.isNotEmpty &&
|
|
_startDateTime != null &&
|
|
_endDateTime != null &&
|
|
_selectedEventTypeId != null &&
|
|
addressController.text.isNotEmpty &&
|
|
(_endDateTime!.isAfter(_startDateTime!));
|
|
}
|
|
|
|
Future<bool> submitForm(BuildContext context, {EventModel? existingEvent}) async {
|
|
if (!validateForm()) {
|
|
_error = "Veuillez remplir tous les champs obligatoires.";
|
|
notifyListeners();
|
|
return false;
|
|
}
|
|
|
|
_isLoading = true;
|
|
_error = null;
|
|
_success = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final eventTypeRef = _selectedEventTypeId != null
|
|
? null // Les références Firestore ne sont plus nécessaires, l'ID suffit
|
|
: null;
|
|
|
|
if (existingEvent != null) {
|
|
// Mode édition
|
|
// Gérer les nouveaux fichiers uploadés s'il y en a
|
|
List<Map<String, String>> finalDocuments = List<Map<String, String>>.from(_uploadedFiles);
|
|
|
|
// Identifier les nouveaux fichiers (ceux qui ont une URL temp)
|
|
final newFiles = _uploadedFiles.where((file) =>
|
|
file['url']?.contains('events/temp/') ?? false).toList();
|
|
|
|
if (newFiles.isNotEmpty) {
|
|
// Déplacer les nouveaux fichiers vers le dossier de l'événement
|
|
final movedFiles = await EventFormService.moveFilesToEvent(newFiles, existingEvent.id);
|
|
|
|
// Remplacer les URLs temporaires par les nouvelles URLs
|
|
for (int i = 0; i < finalDocuments.length; i++) {
|
|
final tempFile = finalDocuments[i];
|
|
final movedFile = movedFiles.firstWhere(
|
|
(moved) => moved['name'] == tempFile['name'],
|
|
orElse: () => tempFile,
|
|
);
|
|
finalDocuments[i] = movedFile;
|
|
}
|
|
}
|
|
|
|
final updatedEvent = EventModel(
|
|
id: existingEvent.id,
|
|
name: nameController.text.trim(),
|
|
description: descriptionController.text.trim(),
|
|
startDateTime: _startDateTime!,
|
|
endDateTime: _endDateTime!,
|
|
basePrice: double.tryParse(basePriceController.text.replaceAll(',', '.')) ?? 0.0,
|
|
installationTime: int.tryParse(installationController.text) ?? 0,
|
|
disassemblyTime: int.tryParse(disassemblyController.text) ?? 0,
|
|
eventTypeId: _selectedEventTypeId!,
|
|
eventTypeRef: eventTypeRef,
|
|
customerId: existingEvent.customerId,
|
|
address: addressController.text.trim(),
|
|
// Envoyer directement les IDs au lieu de DocumentReference pour compatibilité Cloud Functions
|
|
workforce: _selectedUserIds,
|
|
latitude: existingEvent.latitude,
|
|
longitude: existingEvent.longitude,
|
|
documents: finalDocuments,
|
|
options: _selectedOptions,
|
|
status: _selectedStatus,
|
|
jauge: jaugeController.text.isNotEmpty ? int.tryParse(jaugeController.text) : null,
|
|
contactEmail: contactEmailController.text.isNotEmpty ? contactEmailController.text.trim() : null,
|
|
contactPhone: contactPhoneController.text.isNotEmpty ? contactPhoneController.text.trim() : null,
|
|
assignedEquipment: _assignedEquipment,
|
|
assignedContainers: _assignedContainers,
|
|
preparationStatus: existingEvent.preparationStatus,
|
|
returnStatus: existingEvent.returnStatus,
|
|
);
|
|
|
|
await EventFormService.updateEvent(updatedEvent);
|
|
|
|
// Recharger les événements après modification
|
|
final localUserProvider = Provider.of<LocalUserProvider>(context, listen: false);
|
|
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
|
final userId = localUserProvider.uid;
|
|
final canViewAllEvents = localUserProvider.hasPermission('view_all_events');
|
|
|
|
if (userId != null) {
|
|
await eventProvider.loadUserEvents(userId, canViewAllEvents: canViewAllEvents);
|
|
}
|
|
|
|
_success = "Événement modifié avec succès !";
|
|
} else {
|
|
// Mode création
|
|
final newEvent = EventModel(
|
|
id: '',
|
|
name: nameController.text.trim(),
|
|
description: descriptionController.text.trim(),
|
|
startDateTime: _startDateTime!,
|
|
endDateTime: _endDateTime!,
|
|
basePrice: double.tryParse(basePriceController.text.replaceAll(',', '.')) ?? 0.0,
|
|
installationTime: int.tryParse(installationController.text) ?? 0,
|
|
disassemblyTime: int.tryParse(disassemblyController.text) ?? 0,
|
|
eventTypeId: _selectedEventTypeId!,
|
|
eventTypeRef: eventTypeRef,
|
|
customerId: '',
|
|
address: addressController.text.trim(),
|
|
// Envoyer directement les IDs au lieu de DocumentReference pour compatibilité Cloud Functions
|
|
workforce: _selectedUserIds,
|
|
latitude: 0.0,
|
|
longitude: 0.0,
|
|
documents: _uploadedFiles,
|
|
options: _selectedOptions,
|
|
status: _selectedStatus,
|
|
jauge: jaugeController.text.isNotEmpty ? int.tryParse(jaugeController.text) : null,
|
|
contactEmail: contactEmailController.text.isNotEmpty ? contactEmailController.text.trim() : null,
|
|
contactPhone: contactPhoneController.text.isNotEmpty ? contactPhoneController.text.trim() : null,
|
|
assignedContainers: _assignedContainers,
|
|
assignedEquipment: _assignedEquipment,
|
|
);
|
|
|
|
final eventId = await EventFormService.createEvent(newEvent);
|
|
|
|
// Déplacer et mettre à jour les fichiers uniquement s'il y en a
|
|
if (_uploadedFiles.isNotEmpty) {
|
|
final newFiles = await EventFormService.moveFilesToEvent(_uploadedFiles, eventId);
|
|
if (newFiles.isNotEmpty) {
|
|
await EventFormService.updateEventDocuments(eventId, newFiles);
|
|
}
|
|
}
|
|
|
|
// Reload events
|
|
final localUserProvider = Provider.of<LocalUserProvider>(context, listen: false);
|
|
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
|
final userId = localUserProvider.uid;
|
|
final canViewAllEvents = localUserProvider.hasPermission('view_all_events');
|
|
|
|
if (userId != null) {
|
|
await eventProvider.loadUserEvents(userId, canViewAllEvents: canViewAllEvents);
|
|
}
|
|
|
|
_success = "Événement créé avec succès !";
|
|
}
|
|
|
|
_formChanged = false;
|
|
notifyListeners();
|
|
return true;
|
|
} catch (e) {
|
|
_error = "Erreur lors de la sauvegarde : $e";
|
|
notifyListeners();
|
|
return false;
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<bool> deleteEvent(BuildContext context, String eventId) async {
|
|
_isLoading = true;
|
|
_error = null;
|
|
_success = null;
|
|
notifyListeners();
|
|
|
|
try {
|
|
// Supprimer l'événement via l'API
|
|
final dataService = DataService(FirebaseFunctionsApiService());
|
|
await dataService.deleteEvent(eventId);
|
|
|
|
// Recharger la liste des événements
|
|
final localUserProvider = Provider.of<LocalUserProvider>(context, listen: false);
|
|
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
|
final userId = localUserProvider.uid;
|
|
final canViewAllEvents = localUserProvider.hasPermission('view_all_events');
|
|
|
|
if (userId != null) {
|
|
await eventProvider.loadUserEvents(userId, canViewAllEvents: canViewAllEvents);
|
|
}
|
|
|
|
_success = "Événement supprimé avec succès !";
|
|
notifyListeners();
|
|
return true;
|
|
} catch (e) {
|
|
_error = "Erreur lors de la suppression : $e";
|
|
notifyListeners();
|
|
return false;
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
void clearError() {
|
|
_error = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
void clearSuccess() {
|
|
_success = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
nameController.dispose();
|
|
descriptionController.dispose();
|
|
basePriceController.dispose();
|
|
installationController.dispose();
|
|
disassemblyController.dispose();
|
|
addressController.dispose();
|
|
jaugeController.dispose();
|
|
contactEmailController.dispose();
|
|
contactPhoneController.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|