// Vehicle Maintenance Management System (VMMS) // Main JavaScript File class VMMS { constructor() { this.config = this.loadConfig(); this.data = { vehicles: [], parts: [], logs: [] }; this.init(); } // Initialize the application init() { this.setupEventListeners(); this.loadPage('dashboard'); // Load data if config exists if (this.config.sheetId) { this.loadAllData(); } } // Setup all event listeners setupEventListeners() { // Navigation document.querySelectorAll('.nav-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const page = e.target.getAttribute('data-page'); this.loadPage(page); }); }); // Vehicle management document.getElementById('add-vehicle-btn').addEventListener('click', () => this.openVehicleModal()); document.getElementById('vehicle-form').addEventListener('submit', (e) => this.saveVehicle(e)); document.getElementById('vehicle-cancel-btn').addEventListener('click', () => this.closeModal('vehicle-modal')); document.getElementById('vehicle-modal-close').addEventListener('click', () => this.closeModal('vehicle-modal')); // Parts management document.getElementById('parts-vehicle-select').addEventListener('change', (e) => this.loadPartsByVehicle(e.target.value)); document.getElementById('add-part-btn').addEventListener('click', () => this.openPartModal()); document.getElementById('part-form').addEventListener('submit', (e) => this.savePart(e)); document.getElementById('part-cancel-btn').addEventListener('click', () => this.closeModal('part-modal')); document.getElementById('part-modal-close').addEventListener('click', () => this.closeModal('part-modal')); // Logs management document.getElementById('logs-part-select').addEventListener('change', (e) => this.loadLogsByPart(e.target.value)); document.getElementById('add-log-btn').addEventListener('click', () => this.openLogModal()); document.getElementById('log-form').addEventListener('submit', (e) => this.saveLog(e)); document.getElementById('log-cancel-btn').addEventListener('click', () => this.closeModal('log-modal')); document.getElementById('log-modal-close').addEventListener('click', () => this.closeModal('log-modal')); // Configuration document.getElementById('config-form').addEventListener('submit', (e) => this.saveConfig(e)); document.getElementById('test-connection-btn').addEventListener('click', () => this.testConnection()); // Modal click outside to close window.addEventListener('click', (e) => { if (e.target.classList.contains('modal')) { this.closeModal(e.target.id); } }); } // Load and display specific page loadPage(pageId) { // Hide all pages document.querySelectorAll('.page').forEach(page => { page.classList.remove('active'); }); // Remove active class from nav links document.querySelectorAll('.nav-link').forEach(link => { link.classList.remove('active'); }); // Show selected page document.getElementById(pageId).classList.add('active'); document.querySelector(`[data-page="${pageId}"]`).classList.add('active'); // Load page-specific data switch (pageId) { case 'dashboard': this.loadDashboard(); break; case 'vehicles': this.loadVehicles(); break; case 'parts': this.loadParts(); break; case 'logs': this.loadLogs(); break; case 'config': this.loadConfigForm(); break; } } // Configuration Management loadConfig() { const config = localStorage.getItem('vmms-config'); return config ? JSON.parse(config) : { sheetId: '', vehiclesTab: 'Vehicles', partsTab: 'Parts', logsTab: 'MaintenanceLogs' }; } saveConfig(e) { e.preventDefault(); const formData = new FormData(e.target); this.config = { sheetId: formData.get('sheet-id') || document.getElementById('sheet-id').value, vehiclesTab: formData.get('vehicles-tab') || document.getElementById('vehicles-tab').value, partsTab: formData.get('parts-tab') || document.getElementById('parts-tab').value, logsTab: formData.get('logs-tab') || document.getElementById('logs-tab').value }; localStorage.setItem('vmms-config', JSON.stringify(this.config)); this.showToast('Configuration saved successfully!', 'success'); // Load data after saving config this.loadAllData(); } loadConfigForm() { document.getElementById('sheet-id').value = this.config.sheetId; document.getElementById('vehicles-tab').value = this.config.vehiclesTab; document.getElementById('parts-tab').value = this.config.partsTab; document.getElementById('logs-tab').value = this.config.logsTab; } async testConnection() { if (!this.config.sheetId) { this.showToast('Please enter a Sheet ID first', 'error'); return; } this.showLoading(); try { const url = `https://docs.google.com/spreadsheets/d/${this.config.sheetId}/gviz/tq?tqx=out:json&sheet=${this.config.vehiclesTab}`; const response = await fetch(url); if (response.ok) { this.showConnectionStatus('Connection successful! Sheet is accessible.', 'success'); } else { throw new Error('Failed to connect'); } } catch (error) { this.showConnectionStatus('Connection failed. Please check your Sheet ID and permissions.', 'error'); } finally { this.hideLoading(); } } showConnectionStatus(message, type) { const statusDiv = document.getElementById('connection-status'); statusDiv.textContent = message; statusDiv.className = `status-message ${type}`; statusDiv.style.display = 'block'; setTimeout(() => { statusDiv.style.display = 'none'; }, 5000); } // Data Management async loadAllData() { if (!this.config.sheetId) return; this.showLoading(); try { await Promise.all([ this.loadVehiclesData(), this.loadPartsData(), this.loadLogsData() ]); this.loadDashboard(); this.showToast('Data loaded successfully!', 'success'); } catch (error) { console.error('Error loading data:', error); this.showToast('Error loading data. Please check your configuration.', 'error'); } finally { this.hideLoading(); } } async loadVehiclesData() { try { const url = `https://docs.google.com/spreadsheets/d/${this.config.sheetId}/gviz/tq?tqx=out:json&sheet=${this.config.vehiclesTab}`; const response = await fetch(url); const text = await response.text(); const data = this.parseGoogleSheetsData(text); this.data.vehicles = data.map(row => ({ id: row[0], name: row[1], model: row[2], year: row[3], registration: row[4], odometer: row[5], fuelType: row[6] })); } catch (error) { console.error('Error loading vehicles:', error); this.data.vehicles = []; } } async loadPartsData() { try { const url = `https://docs.google.com/spreadsheets/d/${this.config.sheetId}/gviz/tq?tqx=out:json&sheet=${this.config.partsTab}`; const response = await fetch(url); const text = await response.text(); const data = this.parseGoogleSheetsData(text); this.data.parts = data.map(row => ({ id: row[0], vehicleId: row[1], name: row[2], type: row[3], value: row[4], notes: row[5] || '' })); } catch (error) { console.error('Error loading parts:', error); this.data.parts = []; } } async loadLogsData() { try { const url = `https://docs.google.com/spreadsheets/d/${this.config.sheetId}/gviz/tq?tqx=out:json&sheet=${this.config.logsTab}`; const response = await fetch(url); const text = await response.text(); const data = this.parseGoogleSheetsData(text); this.data.logs = data.map(row => ({ id: row[0], partId: row[1], date: row[2], odometer: row[3], notes: row[4] || '' })); } catch (error) { console.error('Error loading logs:', error); this.data.logs = []; } } parseGoogleSheetsData(text) { try { // Remove Google Sheets wrapper const jsonString = text.match(/google\.visualization\.Query\.setResponse\((.*)\);/)[1]; const data = JSON.parse(jsonString); if (!data.table || !data.table.rows) { return []; } return data.table.rows.map(row => row.c ? row.c.map(cell => cell ? cell.v : '') : [] ); } catch (error) { console.error('Error parsing Google Sheets data:', error); return []; } } // Dashboard Management loadDashboard() { const totalVehicles = this.data.vehicles.length; const totalParts = this.data.parts.length; // Calculate overdue and upcoming maintenance const alerts = this.calculateMaintenanceAlerts(); const overdueCount = alerts.filter(alert => alert.type === 'overdue').length; const upcomingCount = alerts.filter(alert => alert.type === 'upcoming').length; // Update dashboard cards document.getElementById('total-vehicles').textContent = totalVehicles; document.getElementById('total-parts').textContent = totalParts; document.getElementById('overdue-parts').textContent = overdueCount; document.getElementById('upcoming-parts').textContent = upcomingCount; // Display alerts this.displayAlerts(alerts); } calculateMaintenanceAlerts() { const alerts = []; const today = new Date(); const oneMonthFromNow = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000); this.data.parts.forEach(part => { const vehicle = this.data.vehicles.find(v => v.id === part.vehicleId); if (!vehicle) return; const partLogs = this.data.logs.filter(log => log.partId === part.id); const lastLog = partLogs.sort((a, b) => new Date(b.date) - new Date(a.date))[0]; if (!lastLog) { // No maintenance history - consider it overdue alerts.push({ type: 'overdue', vehicle: vehicle.name, part: part.name, message: 'No maintenance history' }); return; } const lastLogDate = new Date(lastLog.date); if (part.type === 'month') { const nextDueDate = new Date(lastLogDate.getTime() + parseInt(part.value) * 30 * 24 * 60 * 60 * 1000); if (nextDueDate < today) { alerts.push({ type: 'overdue', vehicle: vehicle.name, part: part.name, message: `Due ${Math.ceil((today - nextDueDate) / (1000 * 60 * 60 * 24))} days ago` }); } else if (nextDueDate < oneMonthFromNow) { alerts.push({ type: 'upcoming', vehicle: vehicle.name, part: part.name, message: `Due in ${Math.ceil((nextDueDate - today) / (1000 * 60 * 60 * 24))} days` }); } } else if (part.type === 'km') { const nextDueKm = parseInt(lastLog.odometer) + parseInt(part.value); const currentKm = parseInt(vehicle.odometer); if (currentKm >= nextDueKm) { alerts.push({ type: 'overdue', vehicle: vehicle.name, part: part.name, message: `Overdue by ${currentKm - nextDueKm} km` }); } else if ((nextDueKm - currentKm) <= 500) { alerts.push({ type: 'upcoming', vehicle: vehicle.name, part: part.name, message: `Due in ${nextDueKm - currentKm} km` }); } } }); return alerts; } displayAlerts(alerts) { const container = document.getElementById('alerts-container'); if (alerts.length === 0) { container.innerHTML = '

No maintenance alerts at this time.

'; return; } const alertsHtml = alerts.map(alert => `
${alert.type === 'overdue' ? '🚨' : '⚠️'}

${alert.vehicle} - ${alert.part}

${alert.message}

`).join(''); container.innerHTML = alertsHtml; } // Vehicle Management loadVehicles() { const tbody = document.querySelector('#vehicles-table tbody'); if (this.data.vehicles.length === 0) { tbody.innerHTML = 'No vehicles found. Add your first vehicle!'; return; } const vehiclesHtml = this.data.vehicles.map(vehicle => ` ${vehicle.id} ${vehicle.name} ${vehicle.model} ${vehicle.year} ${vehicle.registration} ${vehicle.odometer} ${vehicle.fuelType}
`).join(''); tbody.innerHTML = vehiclesHtml; } openVehicleModal(vehicleId = null) { const modal = document.getElementById('vehicle-modal'); const title = document.getElementById('vehicle-modal-title'); const form = document.getElementById('vehicle-form'); form.reset(); if (vehicleId) { const vehicle = this.data.vehicles.find(v => v.id === vehicleId); if (vehicle) { title.textContent = 'Edit Vehicle'; document.getElementById('vehicle-id').value = vehicle.id; document.getElementById('vehicle-name').value = vehicle.name; document.getElementById('vehicle-model').value = vehicle.model; document.getElementById('vehicle-year').value = vehicle.year; document.getElementById('vehicle-registration').value = vehicle.registration; document.getElementById('vehicle-odometer').value = vehicle.odometer; document.getElementById('vehicle-fuel-type').value = vehicle.fuelType; } } else { title.textContent = 'Add Vehicle'; document.getElementById('vehicle-id').value = ''; } modal.style.display = 'block'; } async saveVehicle(e) { e.preventDefault(); const formData = new FormData(e.target); const vehicleId = formData.get('vehicle-id') || document.getElementById('vehicle-id').value; const isEdit = !!vehicleId; const vehicle = { id: vehicleId || this.generateId('V'), name: formData.get('vehicle-name') || document.getElementById('vehicle-name').value, model: formData.get('vehicle-model') || document.getElementById('vehicle-model').value, year: formData.get('vehicle-year') || document.getElementById('vehicle-year').value, registration: formData.get('vehicle-registration') || document.getElementById('vehicle-registration').value, odometer: formData.get('vehicle-odometer') || document.getElementById('vehicle-odometer').value, fuelType: formData.get('vehicle-fuel-type') || document.getElementById('vehicle-fuel-type').value }; try { this.showLoading(); if (isEdit) { await this.updateVehicleInSheet(vehicle); const index = this.data.vehicles.findIndex(v => v.id === vehicleId); this.data.vehicles[index] = vehicle; } else { await this.addVehicleToSheet(vehicle); this.data.vehicles.push(vehicle); } this.closeModal('vehicle-modal'); this.loadVehicles(); this.updateVehicleSelects(); this.showToast(isEdit ? 'Vehicle updated successfully!' : 'Vehicle added successfully!', 'success'); } catch (error) { console.error('Error saving vehicle:', error); this.showToast('Error saving vehicle. Please try again.', 'error'); } finally { this.hideLoading(); } } async addVehicleToSheet(vehicle) { // This is a placeholder - in a real implementation, you would use Google Sheets API // For now, we'll simulate the operation return new Promise(resolve => setTimeout(resolve, 500)); } async updateVehicleInSheet(vehicle) { // This is a placeholder - in a real implementation, you would use Google Sheets API return new Promise(resolve => setTimeout(resolve, 500)); } editVehicle(vehicleId) { this.openVehicleModal(vehicleId); } async deleteVehicle(vehicleId) { if (!confirm('Are you sure you want to delete this vehicle? This will also delete all associated parts and logs.')) { return; } try { this.showLoading(); // Remove vehicle and associated data this.data.vehicles = this.data.vehicles.filter(v => v.id !== vehicleId); this.data.parts = this.data.parts.filter(p => p.vehicleId !== vehicleId); // Remove logs for deleted parts const partIds = this.data.parts.filter(p => p.vehicleId === vehicleId).map(p => p.id); this.data.logs = this.data.logs.filter(l => !partIds.includes(l.partId)); this.loadVehicles(); this.updateVehicleSelects(); this.showToast('Vehicle deleted successfully!', 'success'); } catch (error) { console.error('Error deleting vehicle:', error); this.showToast('Error deleting vehicle. Please try again.', 'error'); } finally { this.hideLoading(); } } // Parts Management loadParts() { this.updateVehicleSelects(); const selectedVehicleId = document.getElementById('parts-vehicle-select').value; this.loadPartsByVehicle(selectedVehicleId); } updateVehicleSelects() { const selects = ['parts-vehicle-select']; selects.forEach(selectId => { const select = document.getElementById(selectId); const currentValue = select.value; select.innerHTML = ''; this.data.vehicles.forEach(vehicle => { const option = document.createElement('option'); option.value = vehicle.id; option.textContent = `${vehicle.name} (${vehicle.registration})`; select.appendChild(option); }); if (currentValue && this.data.vehicles.find(v => v.id === currentValue)) { select.value = currentValue; } }); } loadPartsByVehicle(vehicleId) { const tbody = document.querySelector('#parts-table tbody'); const addButton = document.getElementById('add-part-btn'); if (!vehicleId) { tbody.innerHTML = 'Select a vehicle to view its parts.'; addButton.disabled = true; return; } addButton.disabled = false; const vehicleParts = this.data.parts.filter(p => p.vehicleId === vehicleId); if (vehicleParts.length === 0) { tbody.innerHTML = 'No parts found for this vehicle. Add the first part!'; return; } const partsHtml = vehicleParts.map(part => { const vehicle = this.data.vehicles.find(v => v.id === part.vehicleId); return ` ${part.id} ${vehicle ? vehicle.name : 'Unknown'} ${part.name} ${this.formatReplacementType(part.type)} ${part.value} ${part.notes}
`; }).join(''); tbody.innerHTML = partsHtml; this.updatePartSelects(); } formatReplacementType(type) { switch (type) { case 'month': return 'Time-based (months)'; case 'km': return 'Distance-based (km)'; case 'custom': return 'Custom'; default: return type; } } openPartModal(partId = null) { const selectedVehicleId = document.getElementById('parts-vehicle-select').value; if (!selectedVehicleId && !partId) { this.showToast('Please select a vehicle first', 'warning'); return; } const modal = document.getElementById('part-modal'); const title = document.getElementById('part-modal-title'); const form = document.getElementById('part-form'); form.reset(); if (partId) { const part = this.data.parts.find(p => p.id === partId); if (part) { title.textContent = 'Edit Part'; document.getElementById('part-id').value = part.id; document.getElementById('part-vehicle-id').value = part.vehicleId; document.getElementById('part-name').value = part.name; document.getElementById('part-replacement-type').value = part.type; document.getElementById('part-replacement-value').value = part.value; document.getElementById('part-notes').value = part.notes; } } else { title.textContent = 'Add Part'; document.getElementById('part-id').value = ''; document.getElementById('part-vehicle-id').value = selectedVehicleId; } modal.style.display = 'block'; } async savePart(e) { e.preventDefault(); const formData = new FormData(e.target); const partId = formData.get('part-id') || document.getElementById('part-id').value; const isEdit = !!partId; const part = { id: partId || this.generateId('P'), vehicleId: formData.get('part-vehicle-id') || document.getElementById('part-vehicle-id').value, name: formData.get('part-name') || document.getElementById('part-name').value, type: formData.get('part-replacement-type') || document.getElementById('part-replacement-type').value, value: formData.get('part-replacement-value') || document.getElementById('part-replacement-value').value, notes: formData.get('part-notes') || document.getElementById('part-notes').value }; try { this.showLoading(); if (isEdit) { const index = this.data.parts.findIndex(p => p.id === partId); this.data.parts[index] = part; } else { this.data.parts.push(part); } this.closeModal('part-modal'); this.loadPartsByVehicle(part.vehicleId); this.updatePartSelects(); this.showToast(isEdit ? 'Part updated successfully!' : 'Part added successfully!', 'success'); } catch (error) { console.error('Error saving part:', error); this.showToast('Error saving part. Please try again.', 'error'); } finally { this.hideLoading(); } } editPart(partId) { this.openPartModal(partId); } async deletePart(partId) { if (!confirm('Are you sure you want to delete this part? This will also delete all associated maintenance logs.')) { return; } try { this.showLoading(); const part = this.data.parts.find(p => p.id === partId); // Remove part and associated logs this.data.parts = this.data.parts.filter(p => p.id !== partId); this.data.logs = this.data.logs.filter(l => l.partId !== partId); this.loadPartsByVehicle(part.vehicleId); this.updatePartSelects(); this.showToast('Part deleted successfully!', 'success'); } catch (error) { console.error('Error deleting part:', error); this.showToast('Error deleting part. Please try again.', 'error'); } finally { this.hideLoading(); } } // Logs Management loadLogs() { this.updatePartSelects(); const selectedPartId = document.getElementById('logs-part-select').value; this.loadLogsByPart(selectedPartId); } updatePartSelects() { const select = document.getElementById('logs-part-select'); const currentValue = select.value; select.innerHTML = ''; this.data.parts.forEach(part => { const vehicle = this.data.vehicles.find(v => v.id === part.vehicleId); const option = document.createElement('option'); option.value = part.id; option.textContent = `${vehicle ? vehicle.name : 'Unknown'} - ${part.name}`; select.appendChild(option); }); if (currentValue && this.data.parts.find(p => p.id === currentValue)) { select.value = currentValue; } } loadLogsByPart(partId) { const tbody = document.querySelector('#logs-table tbody'); const addButton = document.getElementById('add-log-btn'); if (!partId) { tbody.innerHTML = 'Select a part to view its maintenance logs.'; addButton.disabled = true; return; } addButton.disabled = false; const partLogs = this.data.logs.filter(l => l.partId === partId); if (partLogs.length === 0) { tbody.innerHTML = 'No maintenance logs found for this part. Add the first log!'; return; } const logsHtml = partLogs.map(log => { const part = this.data.parts.find(p => p.id === log.partId); const vehicle = part ? this.data.vehicles.find(v => v.id === part.vehicleId) : null; return ` ${log.id} ${part ? part.name : 'Unknown'} ${vehicle ? vehicle.name : 'Unknown'} ${this.formatDate(log.date)} ${log.odometer} ${log.notes}
`; }).join(''); tbody.innerHTML = logsHtml; } openLogModal(logId = null) { const selectedPartId = document.getElementById('logs-part-select').value; if (!selectedPartId && !logId) { this.showToast('Please select a part first', 'warning'); return; } const modal = document.getElementById('log-modal'); const title = document.getElementById('log-modal-title'); const form = document.getElementById('log-form'); form.reset(); if (logId) { const log = this.data.logs.find(l => l.id === logId); if (log) { title.textContent = 'Edit Maintenance Log'; document.getElementById('log-id').value = log.id; document.getElementById('log-part-id').value = log.partId; document.getElementById('log-date').value = log.date; document.getElementById('log-odometer').value = log.odometer; document.getElementById('log-notes').value = log.notes; } } else { title.textContent = 'Add Maintenance Log'; document.getElementById('log-id').value = ''; document.getElementById('log-part-id').value = selectedPartId; // Set today's date as default const today = new Date().toISOString().split('T')[0]; document.getElementById('log-date').value = today; } modal.style.display = 'block'; } async saveLog(e) { e.preventDefault(); const formData = new FormData(e.target); const logId = formData.get('log-id') || document.getElementById('log-id').value; const isEdit = !!logId; const log = { id: logId || this.generateId('L'), partId: formData.get('log-part-id') || document.getElementById('log-part-id').value, date: formData.get('log-date') || document.getElementById('log-date').value, odometer: formData.get('log-odometer') || document.getElementById('log-odometer').value, notes: formData.get('log-notes') || document.getElementById('log-notes').value }; try { this.showLoading(); if (isEdit) { const index = this.data.logs.findIndex(l => l.id === logId); this.data.logs[index] = log; } else { this.data.logs.push(log); } this.closeModal('log-modal'); this.loadLogsByPart(log.partId); this.showToast(isEdit ? 'Log updated successfully!' : 'Log added successfully!', 'success'); } catch (error) { console.error('Error saving log:', error); this.showToast('Error saving log. Please try again.', 'error'); } finally { this.hideLoading(); } } editLog(logId) { this.openLogModal(logId); } async deleteLog(logId) { if (!confirm('Are you sure you want to delete this maintenance log?')) { return; } try { this.showLoading(); const log = this.data.logs.find(l => l.id === logId); this.data.logs = this.data.logs.filter(l => l.id !== logId); this.loadLogsByPart(log.partId); this.showToast('Log deleted successfully!', 'success'); } catch (error) { console.error('Error deleting log:', error); this.showToast('Error deleting log. Please try again.', 'error'); } finally { this.hideLoading(); } } // Utility Functions generateId(prefix) { const items = this.data[prefix === 'V' ? 'vehicles' : prefix === 'P' ? 'parts' : 'logs']; const numbers = items .map(item => parseInt(item.id.substring(1))) .filter(num => !isNaN(num)) .sort((a, b) => b - a); const nextNumber = numbers.length > 0 ? numbers[0] + 1 : 1; return `${prefix}${nextNumber}`; } formatDate(dateString) { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleDateString(); } closeModal(modalId) { document.getElementById(modalId).style.display = 'none'; } showLoading() { document.getElementById('loading-overlay').style.display = 'flex'; } hideLoading() { document.getElementById('loading-overlay').style.display = 'none'; } showToast(message, type = 'info') { const toastContainer = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.innerHTML = `
${type.charAt(0).toUpperCase() + type.slice(1)}
${message}
`; toastContainer.appendChild(toast); // Auto-remove toast after 5 seconds setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 5000); // Add click to dismiss toast.addEventListener('click', () => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }); } } // Initialize the application when the DOM is loaded document.addEventListener('DOMContentLoaded', () => { window.vmms = new VMMS(); }); // Additional helper functions for Google Sheets integration // Note: In a real implementation, you would need to set up proper Google Sheets API integration // This includes authentication, proper API calls, and error handling class GoogleSheetsAPI { constructor(sheetId, apiKey = null) { this.sheetId = sheetId; this.apiKey = apiKey; this.baseUrl = 'https://sheets.googleapis.com/v4/spreadsheets'; } async readSheet(sheetName) { try { const url = this.apiKey ? `${this.baseUrl}/${this.sheetId}/values/${sheetName}?key=${this.apiKey}` : `https://docs.google.com/spreadsheets/d/${this.sheetId}/gviz/tq?tqx=out:json&sheet=${sheetName}`; const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } if (this.apiKey) { const data = await response.json(); return data.values || []; } else { const text = await response.text(); return this.parseGoogleSheetsData(text); } } catch (error) { console.error(`Error reading sheet ${sheetName}:`, error); throw error; } } async appendToSheet(sheetName, values) { if (!this.apiKey) { throw new Error('API key required for write operations'); } try { const url = `${this.baseUrl}/${this.sheetId}/values/${sheetName}!A:A:append?valueInputOption=USER_ENTERED&key=${this.apiKey}`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ values: [values] }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { console.error(`Error appending to sheet ${sheetName}:`, error); throw error; } } async updateSheet(sheetName, range, values) { if (!this.apiKey) { throw new Error('API key required for write operations'); } try { const url = `${this.baseUrl}/${this.sheetId}/values/${sheetName}!${range}?valueInputOption=USER_ENTERED&key=${this.apiKey}`; const response = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ values: [values] }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { console.error(`Error updating sheet ${sheetName}:`, error); throw error; } } parseGoogleSheetsData(text) { try { const jsonString = text.match(/google\.visualization\.Query\.setResponse\((.*)\);/)[1]; const data = JSON.parse(jsonString); if (!data.table || !data.table.rows) { return []; } return data.table.rows.map(row => row.c ? row.c.map(cell => cell ? cell.v : '') : [] ); } catch (error) { console.error('Error parsing Google Sheets data:', error); return []; } } } // Export for potential use in modules if (typeof module !== 'undefined' && module.exports) { module.exports = { VMMS, GoogleSheetsAPI }; }