Spaces:
Running
Running
| // 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 = '<p class="no-alerts">No maintenance alerts at this time.</p>'; | |
| return; | |
| } | |
| const alertsHtml = alerts.map(alert => ` | |
| <div class="alert-item ${alert.type}"> | |
| <div class="alert-icon"> | |
| ${alert.type === 'overdue' ? '🚨' : '⚠️'} | |
| </div> | |
| <div class="alert-content"> | |
| <h4>${alert.vehicle} - ${alert.part}</h4> | |
| <p>${alert.message}</p> | |
| </div> | |
| </div> | |
| `).join(''); | |
| container.innerHTML = alertsHtml; | |
| } | |
| // Vehicle Management | |
| loadVehicles() { | |
| const tbody = document.querySelector('#vehicles-table tbody'); | |
| if (this.data.vehicles.length === 0) { | |
| tbody.innerHTML = '<tr class="no-data"><td colspan="8">No vehicles found. Add your first vehicle!</td></tr>'; | |
| return; | |
| } | |
| const vehiclesHtml = this.data.vehicles.map(vehicle => ` | |
| <tr> | |
| <td>${vehicle.id}</td> | |
| <td>${vehicle.name}</td> | |
| <td>${vehicle.model}</td> | |
| <td>${vehicle.year}</td> | |
| <td>${vehicle.registration}</td> | |
| <td>${vehicle.odometer}</td> | |
| <td>${vehicle.fuelType}</td> | |
| <td> | |
| <div class="action-buttons"> | |
| <button class="btn btn-warning btn-small" onclick="vmms.editVehicle('${vehicle.id}')">Edit</button> | |
| <button class="btn btn-danger btn-small" onclick="vmms.deleteVehicle('${vehicle.id}')">Delete</button> | |
| </div> | |
| </td> | |
| </tr> | |
| `).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 = '<option value="">Select Vehicle</option>'; | |
| 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 = '<tr class="no-data"><td colspan="7">Select a vehicle to view its parts.</td></tr>'; | |
| addButton.disabled = true; | |
| return; | |
| } | |
| addButton.disabled = false; | |
| const vehicleParts = this.data.parts.filter(p => p.vehicleId === vehicleId); | |
| if (vehicleParts.length === 0) { | |
| tbody.innerHTML = '<tr class="no-data"><td colspan="7">No parts found for this vehicle. Add the first part!</td></tr>'; | |
| return; | |
| } | |
| const partsHtml = vehicleParts.map(part => { | |
| const vehicle = this.data.vehicles.find(v => v.id === part.vehicleId); | |
| return ` | |
| <tr> | |
| <td>${part.id}</td> | |
| <td>${vehicle ? vehicle.name : 'Unknown'}</td> | |
| <td>${part.name}</td> | |
| <td>${this.formatReplacementType(part.type)}</td> | |
| <td>${part.value}</td> | |
| <td>${part.notes}</td> | |
| <td> | |
| <div class="action-buttons"> | |
| <button class="btn btn-warning btn-small" onclick="vmms.editPart('${part.id}')">Edit</button> | |
| <button class="btn btn-danger btn-small" onclick="vmms.deletePart('${part.id}')">Delete</button> | |
| </div> | |
| </td> | |
| </tr> | |
| `; | |
| }).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 = '<option value="">Select Part</option>'; | |
| 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 = '<tr class="no-data"><td colspan="7">Select a part to view its maintenance logs.</td></tr>'; | |
| addButton.disabled = true; | |
| return; | |
| } | |
| addButton.disabled = false; | |
| const partLogs = this.data.logs.filter(l => l.partId === partId); | |
| if (partLogs.length === 0) { | |
| tbody.innerHTML = '<tr class="no-data"><td colspan="7">No maintenance logs found for this part. Add the first log!</td></tr>'; | |
| 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 ` | |
| <tr> | |
| <td>${log.id}</td> | |
| <td>${part ? part.name : 'Unknown'}</td> | |
| <td>${vehicle ? vehicle.name : 'Unknown'}</td> | |
| <td>${this.formatDate(log.date)}</td> | |
| <td>${log.odometer}</td> | |
| <td>${log.notes}</td> | |
| <td> | |
| <div class="action-buttons"> | |
| <button class="btn btn-warning btn-small" onclick="vmms.editLog('${log.id}')">Edit</button> | |
| <button class="btn btn-danger btn-small" onclick="vmms.deleteLog('${log.id}')">Delete</button> | |
| </div> | |
| </td> | |
| </tr> | |
| `; | |
| }).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 = ` | |
| <div class="toast-title">${type.charAt(0).toUpperCase() + type.slice(1)}</div> | |
| <div class="toast-message">${message}</div> | |
| `; | |
| 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 }; | |
| } |