Spaces:
Runtime error
Runtime error
| /** | |
| * Form Manager Module | |
| * Handles form operations, validation, and data management | |
| */ | |
| export class FormManager { | |
| constructor(apiClient, uiManager) { | |
| this.apiClient = apiClient; | |
| this.uiManager = uiManager; | |
| this.uploadedPhotos = {}; | |
| this.audioFile = null; | |
| this.currentEditId = null; | |
| } | |
| async initialize() { | |
| try { | |
| const formOptions = await this.apiClient.loadFormOptions(); | |
| this.renderMultiSelect('utilityOptions', formOptions.utilities); | |
| this.renderMultiSelect('phenologyOptions', formOptions.phenologyStages); | |
| this.renderPhotoCategories(formOptions.photoCategories); | |
| } catch (error) { | |
| console.error('Error initializing form:', error); | |
| throw error; | |
| } | |
| } | |
| renderMultiSelect(containerId, options) { | |
| const container = document.getElementById(containerId); | |
| if (!container) return; | |
| container.innerHTML = ''; | |
| options.forEach(option => { | |
| const label = document.createElement('label'); | |
| label.innerHTML = ` | |
| <input type="checkbox" value="${option}"> ${option} | |
| `; | |
| container.appendChild(label); | |
| }); | |
| } | |
| renderPhotoCategories(categories) { | |
| const container = document.getElementById('photoCategories'); | |
| if (!container) return; | |
| container.innerHTML = ''; | |
| categories.forEach(category => { | |
| const categoryDiv = document.createElement('div'); | |
| categoryDiv.className = 'photo-category'; | |
| categoryDiv.innerHTML = ` | |
| <div class="photo-category-header"> | |
| <div class="photo-category-title"> | |
| <div class="photo-category-icon">IMG</div> | |
| ${category} | |
| </div> | |
| </div> | |
| <div class="photo-upload-area"> | |
| <div class="photo-upload" data-category="${category}"> | |
| <div class="photo-upload-icon">+</div> | |
| <div>Click to select ${category} photo</div> | |
| </div> | |
| <button type="button" class="camera-btn" data-category="${category}"> | |
| Camera | |
| </button> | |
| </div> | |
| <div class="uploaded-file" id="photo-${category}" style="display: none;"></div> | |
| `; | |
| container.appendChild(categoryDiv); | |
| }); | |
| } | |
| getSelectedValues(containerId) { | |
| const container = document.getElementById(containerId); | |
| if (!container) return []; | |
| const checkboxes = container.querySelectorAll('input[type="checkbox"]:checked'); | |
| return Array.from(checkboxes).map(cb => cb.value); | |
| } | |
| getFormData() { | |
| const utilityValues = this.getSelectedValues('utilityOptions'); | |
| const phenologyValues = this.getSelectedValues('phenologyOptions'); | |
| return { | |
| latitude: this.getNumericValue('latitude'), | |
| longitude: this.getNumericValue('longitude'), | |
| location_name: this.getStringValue('locationName'), | |
| local_name: this.getStringValue('localName'), | |
| scientific_name: this.getStringValue('scientificName'), | |
| common_name: this.getStringValue('commonName'), | |
| tree_code: this.getStringValue('treeCode'), | |
| height: this.getNumericValue('height'), | |
| width: this.getNumericValue('width'), | |
| utility: utilityValues.length > 0 ? utilityValues : [], | |
| phenology_stages: phenologyValues.length > 0 ? phenologyValues : [], | |
| storytelling_text: this.getStringValue('storytellingText'), | |
| storytelling_audio: this.audioFile, | |
| photographs: Object.keys(this.uploadedPhotos).length > 0 ? this.uploadedPhotos : null, | |
| notes: this.getStringValue('notes') | |
| }; | |
| } | |
| getNumericValue(fieldId) { | |
| const element = document.getElementById(fieldId); | |
| if (!element || !element.value) return null; | |
| const value = parseFloat(element.value); | |
| return isNaN(value) ? null : value; | |
| } | |
| getStringValue(fieldId) { | |
| const element = document.getElementById(fieldId); | |
| return element && element.value ? element.value : null; | |
| } | |
| populateForm(treeData) { | |
| this.setFieldValue('latitude', treeData.latitude); | |
| this.setFieldValue('longitude', treeData.longitude); | |
| this.setFieldValue('locationName', treeData.location_name || ''); | |
| this.setFieldValue('localName', treeData.local_name || ''); | |
| this.setFieldValue('scientificName', treeData.scientific_name || ''); | |
| this.setFieldValue('commonName', treeData.common_name || ''); | |
| this.setFieldValue('treeCode', treeData.tree_code || ''); | |
| this.setFieldValue('height', treeData.height || ''); | |
| this.setFieldValue('width', treeData.width || ''); | |
| this.setFieldValue('storytellingText', treeData.storytelling_text || ''); | |
| this.setFieldValue('notes', treeData.notes || ''); | |
| // Handle utility checkboxes | |
| if (treeData.utility && Array.isArray(treeData.utility)) { | |
| this.setCheckboxValues('utilityOptions', treeData.utility); | |
| } | |
| // Handle phenology checkboxes | |
| if (treeData.phenology_stages && Array.isArray(treeData.phenology_stages)) { | |
| this.setCheckboxValues('phenologyOptions', treeData.phenology_stages); | |
| } | |
| } | |
| setFieldValue(fieldId, value) { | |
| const element = document.getElementById(fieldId); | |
| if (element) { | |
| element.value = value; | |
| } | |
| } | |
| setCheckboxValues(containerId, values) { | |
| const container = document.getElementById(containerId); | |
| if (!container) return; | |
| container.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { | |
| checkbox.checked = values.includes(checkbox.value); | |
| }); | |
| } | |
| resetForm(silent = false) { | |
| const form = document.getElementById('treeForm'); | |
| if (form) { | |
| form.reset(); | |
| } | |
| this.uploadedPhotos = {}; | |
| this.audioFile = null; | |
| this.currentEditId = null; | |
| // Clear uploaded file indicators | |
| document.querySelectorAll('.uploaded-file').forEach(el => { | |
| el.style.display = 'none'; | |
| el.innerHTML = ''; | |
| }); | |
| // Reset audio controls | |
| this.resetAudioControls(); | |
| if (!silent) { | |
| this.uiManager.showMessage('Form has been reset.', 'success'); | |
| } | |
| } | |
| resetAudioControls() { | |
| const audioElement = document.getElementById('audioPlayback'); | |
| if (audioElement) { | |
| audioElement.classList.add('hidden'); | |
| audioElement.src = ''; | |
| } | |
| const recordingStatus = document.getElementById('recordingStatus'); | |
| if (recordingStatus) { | |
| recordingStatus.textContent = 'Click to start recording'; | |
| } | |
| const audioUploadResult = document.getElementById('audioUploadResult'); | |
| if (audioUploadResult) { | |
| audioUploadResult.innerHTML = ''; | |
| } | |
| } | |
| async handleFileUpload(file, type, category = null) { | |
| try { | |
| const result = await this.apiClient.uploadFile(file, type, category); | |
| if (type === 'image' && category) { | |
| this.uploadedPhotos[category] = result.filename; | |
| this.showUploadSuccess(category, file.name, 'photo'); | |
| } else if (type === 'audio') { | |
| this.audioFile = result.filename; | |
| this.showUploadSuccess(null, file.name, 'audio'); | |
| } | |
| return result; | |
| } catch (error) { | |
| console.error(`Error uploading ${type}:`, error); | |
| this.uiManager.showMessage(`Error uploading ${type}: ${error.message}`, 'error'); | |
| throw error; | |
| } | |
| } | |
| showUploadSuccess(category, filename, type) { | |
| if (type === 'photo' && category) { | |
| const resultDiv = document.getElementById(`photo-${category}`); | |
| if (resultDiv) { | |
| resultDiv.style.display = 'block'; | |
| resultDiv.innerHTML = `${filename} uploaded successfully`; | |
| } | |
| } else if (type === 'audio') { | |
| const resultDiv = document.getElementById('audioUploadResult'); | |
| if (resultDiv) { | |
| resultDiv.innerHTML = `<div class="uploaded-file">${filename} uploaded successfully</div>`; | |
| } | |
| } | |
| } | |
| setEditMode(treeId) { | |
| this.currentEditId = treeId; | |
| // Update submit button (only if not demo user) | |
| const submitBtn = document.querySelector('button[type="submit"]'); | |
| if (submitBtn && !submitBtn.getAttribute('data-demo-disabled')) { | |
| submitBtn.textContent = 'Update Tree Record'; | |
| } | |
| // Add cancel edit button if it doesn't exist | |
| this.addCancelButton(); | |
| } | |
| addCancelButton() { | |
| if (document.getElementById('cancelEdit')) return; | |
| const cancelBtn = document.createElement('button'); | |
| cancelBtn.type = 'button'; | |
| cancelBtn.id = 'cancelEdit'; | |
| cancelBtn.className = 'tt-btn tt-btn-outline'; | |
| cancelBtn.textContent = 'Cancel Edit'; | |
| const formActions = document.querySelector('.form-actions'); | |
| const submitBtn = document.querySelector('button[type="submit"]'); | |
| if (formActions && submitBtn) { | |
| formActions.insertBefore(cancelBtn, submitBtn); | |
| } | |
| } | |
| exitEditMode() { | |
| this.currentEditId = null; | |
| // Restore original submit button text (only if not demo user) | |
| const submitBtn = document.querySelector('button[type="submit"]'); | |
| if (submitBtn && !submitBtn.getAttribute('data-demo-disabled')) { | |
| submitBtn.textContent = 'Save Tree Record'; | |
| } | |
| // Remove cancel button | |
| const cancelBtn = document.getElementById('cancelEdit'); | |
| if (cancelBtn) { | |
| cancelBtn.remove(); | |
| } | |
| } | |
| isInEditMode() { | |
| return this.currentEditId !== null; | |
| } | |
| getCurrentEditId() { | |
| return this.currentEditId; | |
| } | |
| validateForm() { | |
| const latitude = this.getNumericValue('latitude'); | |
| const longitude = this.getNumericValue('longitude'); | |
| if (latitude === null || longitude === null) { | |
| throw new Error('Latitude and longitude are required'); | |
| } | |
| if (latitude < -90 || latitude > 90) { | |
| throw new Error('Latitude must be between -90 and 90 degrees'); | |
| } | |
| if (longitude < -180 || longitude > 180) { | |
| throw new Error('Longitude must be between -180 and 180 degrees'); | |
| } | |
| return true; | |
| } | |
| } | |