/** * Landvex Dashboard * Interactive web dashboard for urban intelligence */ // API Configuration const API_URL = 'https://api.landvex.com/v1'; // State let currentView = 'overview'; let selectedLocation = null; let observations = []; let map = null; let charts = {}; // Initialize dashboard document.addEventListener('DOMContentLoaded', () => { initDashboard(); }); function initDashboard() { loadStats(); loadObservations(); initMap(); initCharts(); setupEventListeners(); startRealtimeUpdates(); } // Load statistics async function loadStats() { try { const response = await fetch(`${API_URL}/stats`); const data = await response.json(); updateStatCard('total-observations', data.total_observations, '+12%'); updateStatCard('active-zoomers', data.active_zoomers, '+8%'); updateStatCard('avg-rgi', data.avg_rgi.toFixed(1), '+0.3'); updateStatCard('pending-actions', data.pending_actions, '-5'); } catch (error) { console.log('Using mock data'); updateStatCard('total-observations', 1247, '+12%'); updateStatCard('active-zoomers', 89, '+8%'); updateStatCard('avg-rgi', 4.2, '+0.3'); updateStatCard('pending-actions', 23, '-5'); } } function updateStatCard(id, value, change) { const card = document.getElementById(id); if (!card) return; const valueEl = card.querySelector('.stat-value'); const changeEl = card.querySelector('.stat-change'); if (valueEl) valueEl.textContent = value.toLocaleString(); if (changeEl) { changeEl.textContent = change; changeEl.className = 'stat-change ' + (change.startsWith('+') ? 'positive' : 'negative'); } } // Load observations async function loadObservations() { try { const response = await fetch(`${API_URL}/observations?limit=10`); const data = await response.json(); observations = data.observations || []; renderObservationsTable(); } catch (error) { console.log('Using mock observations'); observations = getMockObservations(); renderObservationsTable(); } } function getMockObservations() { return [ { id: 'OBS-001247', lat: 59.3293, lng: 18.0686, type: 'Street Light', condition: 3, zoomer: 'Marcus K.', status: 'approved', date: '2026-06-26' }, { id: 'OBS-001246', lat: 59.3301, lng: 18.0692, type: 'Sidewalk', condition: 4, zoomer: 'Sophie V.', status: 'pending', date: '2026-06-26' }, { id: 'OBS-001245', lat: 59.3289, lng: 18.0678, type: 'Building', condition: 2, zoomer: 'Alicia L.', status: 'approved', date: '2026-06-25' }, { id: 'OBS-001244', lat: 59.3312, lng: 18.0701, type: 'Road', condition: 5, zoomer: 'James R.', status: 'pending', date: '2026-06-25' }, { id: 'OBS-001243', lat: 59.3278, lng: 18.0665, type: 'Bridge', condition: 3, zoomer: 'Emma T.', status: 'rejected', date: '2026-06-24' }, ]; } function renderObservationsTable() { const tbody = document.getElementById('observations-tbody'); if (!tbody) return; tbody.innerHTML = observations.map(obs => ` ${obs.id} ${obs.lat.toFixed(4)}, ${obs.lng.toFixed(4)} ${obs.type} ${obs.condition}/5 ${obs.zoomer} ${obs.status} ${obs.date} `).join(''); // Add click handlers tbody.querySelectorAll('tr').forEach(row => { row.addEventListener('click', () => { const id = row.dataset.id; showObservationDetail(id); }); }); } // Initialize map (placeholder for real map integration) function initMap() { const mapContainer = document.getElementById('map-container'); if (!mapContainer) return; // Placeholder - integrate with Mapbox, Google Maps, or Leaflet mapContainer.innerHTML = `
🗺️

Interactive Map

${observations.length} observations

`; } function loadRealMap() { // Integrate with actual map library console.log('Loading real map...'); } // Initialize charts (placeholder for Chart.js or similar) function initCharts() { const rgiChart = document.getElementById('rgi-chart'); if (rgiChart) { renderRGIChart(); } } function renderRGIChart() { const dimensions = [ { name: 'Physical', value: 5.5, color: '#007AFF' }, { name: 'Organizational', value: 3.8, color: '#34C759' }, { name: 'Safety', value: 4.1, color: '#FF9500' }, { name: 'Economic', value: 2.9, color: '#FF3B30' }, { name: 'Maintenance', value: 4.7, color: '#5856D6' }, { name: 'Environmental', value: 3.2, color: '#AF52DE' }, { name: 'Informational', value: 3.5, color: '#5AC8FA' }, ]; const chartContainer = document.getElementById('rgi-chart'); if (!chartContainer) return; chartContainer.innerHTML = dimensions.map(d => `
${d.name}
${d.value}
`).join(''); } // Setup event listeners function setupEventListeners() { // Navigation document.querySelectorAll('.nav-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const view = e.target.dataset.view; switchView(view); }); }); // Filter buttons document.querySelectorAll('.filter-btn').forEach(btn => { btn.addEventListener('click', (e) => { const filter = e.target.dataset.filter; applyFilter(filter); }); }); // Export button const exportBtn = document.getElementById('export-btn'); if (exportBtn) { exportBtn.addEventListener('click', exportData); } } // Switch view function switchView(view) { currentView = view; // Hide all views document.querySelectorAll('.view').forEach(v => v.classList.remove('active')); // Show selected view const selectedView = document.getElementById(`view-${view}`); if (selectedView) { selectedView.classList.add('active'); } // Update nav document.querySelectorAll('.nav-link').forEach(link => { link.classList.toggle('active', link.dataset.view === view); }); } // Apply filter function applyFilter(filter) { console.log('Applying filter:', filter); // Filter observations and update table } // Show observation detail function showObservationDetail(id) { const obs = observations.find(o => o.id === id); if (!obs) return; // Show modal or navigate to detail view console.log('Observation detail:', obs); } // Export data function exportData() { const data = { observations, exported_at: new Date().toISOString(), }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `landvex-export-${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); } // Real-time updates function startRealtimeUpdates() { // Poll for new observations every 30 seconds setInterval(() => { loadObservations(); }, 30000); // Update stats every minute setInterval(() => { loadStats(); }, 60000); } // Search functionality function searchObservations(query) { const filtered = observations.filter(obs => obs.id.toLowerCase().includes(query.toLowerCase()) || obs.type.toLowerCase().includes(query.toLowerCase()) || obs.zoomer.toLowerCase().includes(query.toLowerCase()) ); renderFilteredObservations(filtered); } function renderFilteredObservations(filtered) { const tbody = document.getElementById('observations-tbody'); if (!tbody) return; tbody.innerHTML = filtered.map(obs => ` ${obs.id} ${obs.lat.toFixed(4)}, ${obs.lng.toFixed(4)} ${obs.type} ${obs.condition}/5 ${obs.zoomer} ${obs.status} ${obs.date} `).join(''); } // RGI calculation function calculateRGI(observations) { if (!observations.length) return 0; const conditions = observations.map(o => o.condition); const avgCondition = conditions.reduce((a, b) => a + b, 0) / conditions.length; // RGI = 7 - average condition (inverted) return Math.max(0, 7 - avgCondition).toFixed(1); } // Export for global access window.LandvexDashboard = { switchView, applyFilter, exportData, searchObservations, calculateRGI, };