/** * API Client Module * Handles all API communication with authentication */ export class ApiClient { constructor(authManager) { this.authManager = authManager; } async authenticatedFetch(url, options = {}) { const headers = { ...this.authManager.getAuthHeaders(), ...options.headers }; const response = await fetch(url, { ...options, headers }); if (response.status === 401) { // Token expired or invalid this.authManager.clearAuthData(); window.location.href = '/login'; return null; } return response; } async loadFormOptions() { try { const [utilityResponse, phenologyResponse, categoriesResponse] = await Promise.all([ this.authenticatedFetch('/api/utilities'), this.authenticatedFetch('/api/phenology-stages'), this.authenticatedFetch('/api/photo-categories') ]); if (!utilityResponse || !phenologyResponse || !categoriesResponse) { throw new Error('Failed to load form options'); } const [utilityData, phenologyData, categoriesData] = await Promise.all([ utilityResponse.json(), phenologyResponse.json(), categoriesResponse.json() ]); return { utilities: utilityData.utilities, phenologyStages: phenologyData.stages, photoCategories: categoriesData.categories }; } catch (error) { console.error('Error loading form options:', error); throw error; } } async saveTree(treeData) { const response = await this.authenticatedFetch('/api/trees', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(treeData) }); if (!response) return null; if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Unknown error'); } return await response.json(); } async updateTree(treeId, treeData) { const response = await this.authenticatedFetch(`/api/trees/${treeId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(treeData) }); if (!response) return null; if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Unknown error'); } return await response.json(); } async deleteTree(treeId) { const response = await this.authenticatedFetch(`/api/trees/${treeId}`, { method: 'DELETE' }); if (!response) return null; if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Unknown error'); } return true; } async loadTrees(limit = 20) { const response = await this.authenticatedFetch(`/api/trees?limit=${limit}`); if (!response) return []; if (!response.ok) { throw new Error('Failed to load trees'); } return await response.json(); } async loadTree(treeId) { const response = await this.authenticatedFetch(`/api/trees/${treeId}`); if (!response) return null; if (!response.ok) { throw new Error('Failed to fetch tree data'); } return await response.json(); } async loadTreeCodes() { const response = await this.authenticatedFetch('/api/tree-codes'); if (!response) return []; if (!response.ok) { throw new Error('Failed to load tree codes'); } const data = await response.json(); return data.tree_codes || []; } async searchTreeSuggestions(query, limit = 10) { const response = await this.authenticatedFetch( `/api/tree-suggestions?query=${encodeURIComponent(query)}&limit=${limit}` ); if (!response) return []; if (!response.ok) { throw new Error('Failed to search tree suggestions'); } const data = await response.json(); return data.suggestions || []; } async uploadFile(file, type, category = null) { const formData = new FormData(); formData.append('file', file); if (category) { formData.append('category', category); } const endpoint = type === 'image' ? '/api/upload/image' : '/api/upload/audio'; let resJson = null; try { const response = await fetch(endpoint, { method: 'POST', headers: { 'Authorization': `Bearer ${this.authManager.authToken}` }, body: formData }); if (!response.ok) { // Avoid noisy telemetry; just throw error throw new Error('Upload failed'); } resJson = await response.json(); return resJson; } catch (e) { // Network or other errors throw e; } } }