diff --git a/map/app/controllers/dashboardController.js b/map/app/controllers/dashboardController.js index 1e9e3bf..1924d26 100644 --- a/map/app/controllers/dashboardController.js +++ b/map/app/controllers/dashboardController.js @@ -5,20 +5,36 @@ const config = require('../config'); class DashboardController { async getStats(req, res) { try { - // Get all locations for support level stats - const locationsResponse = await nocodbService.getAll(config.nocodb.tableId); + // Get all locations using the paginated method + const locationsResponse = await nocodbService.getAllPaginated(config.nocodb.tableId); const locations = locationsResponse.list || []; + logger.info(`Processing ${locations.length} locations for dashboard stats`); + // Calculate support level distribution const supportLevels = { '1': 0, '2': 0, '3': 0, '4': 0 }; - let signRequests = 0; + let signDelivered = 0; + + // Track sign sizes for requested signs + const signSizes = { 'Regular': 0, 'Large': 0, 'Unsure': 0 }; locations.forEach(loc => { + // Support levels if (loc['Support Level']) { supportLevels[loc['Support Level']]++; } + + // Signs delivered (where Sign checkbox is checked) if (loc.Sign || loc.sign) { - signRequests++; + signDelivered++; + } + + // Sign sizes for requested signs (count all with sign size, regardless of delivery) + if (loc['Sign Size']) { + const size = loc['Sign Size']; + if (signSizes.hasOwnProperty(size)) { + signSizes[size]++; + } } }); @@ -28,9 +44,15 @@ class DashboardController { supportLevels['3'] * 2 + supportLevels['4'] * 1) / (totalResponses || 1); - // Get user stats - const usersResponse = await nocodbService.getAll(config.nocodb.loginSheetId); - const users = usersResponse.list || []; + // Get all users using the paginated method + let users = []; + if (config.nocodb.loginSheetId) { + const usersResponse = await nocodbService.getAllPaginated(config.nocodb.loginSheetId); + users = usersResponse.list || []; + logger.info(`Processing ${users.length} users for dashboard stats`); + } else { + logger.warn('Login sheet ID not configured, skipping user stats'); + } // Get daily entry counts for the last 30 days const thirtyDaysAgo = new Date(); @@ -38,8 +60,8 @@ class DashboardController { const dailyEntries = {}; locations.forEach(loc => { - const createdAt = new Date(loc.CreatedAt || loc.created_at); - if (createdAt >= thirtyDaysAgo) { + const createdAt = new Date(loc.CreatedAt || loc.created_at || loc.createdAt); + if (!isNaN(createdAt.getTime()) && createdAt >= thirtyDaysAgo) { const dateKey = createdAt.toISOString().split('T')[0]; dailyEntries[dateKey] = (dailyEntries[dateKey] || 0) + 1; } @@ -49,7 +71,8 @@ class DashboardController { success: true, data: { supportLevels, - signRequests, + signDelivered, + signSizes, totalLocations: locations.length, overallScore: weightedScore.toFixed(2), totalUsers: users.length, diff --git a/map/app/public/admin.html b/map/app/public/admin.html index 304c09d..23f518b 100644 --- a/map/app/public/admin.html +++ b/map/app/public/admin.html @@ -88,8 +88,8 @@
out of 4.0
-

Sign Requests

-
-
+

Signs Delivered

+
-

Total Users

@@ -104,6 +104,10 @@
+

Sign Sizes Requested

+ +
+

Daily Entries (Last 30 Days)

diff --git a/map/app/public/css/modules/dashboard.css b/map/app/public/css/modules/dashboard.css index 37c5e56..8f4772c 100644 --- a/map/app/public/css/modules/dashboard.css +++ b/map/app/public/css/modules/dashboard.css @@ -55,6 +55,10 @@ min-height: 350px; } +.chart-container.chart-full-width { + grid-column: 1 / -1; +} + .chart-container h3 { margin: 0 0 1.5rem 0; font-size: 1.25rem; @@ -68,6 +72,17 @@ touch-action: pan-y; /* Allow vertical scrolling on mobile */ } +/* Responsive design */ +@media (max-width: 1200px) { + .dashboard-charts { + grid-template-columns: 1fr; + } + + .chart-container.chart-full-width { + grid-column: 1; + } +} + /* Responsive design */ @media (max-width: 768px) { .dashboard-container { diff --git a/map/app/public/js/dashboard.js b/map/app/public/js/dashboard.js index 0357145..539f0dc 100644 --- a/map/app/public/js/dashboard.js +++ b/map/app/public/js/dashboard.js @@ -1,16 +1,21 @@ // Dashboard functionality let supportChart = null; let entriesChart = null; +let signSizesChart = null; // Load dashboard data async function loadDashboardData() { try { + // Show loading state + setLoadingState(true); + const response = await fetch('/api/admin/dashboard/stats'); const result = await response.json(); if (result.success) { updateDashboardCards(result.data); createSupportLevelChart(result.data.supportLevels); + createSignSizesChart(result.data.signSizes); createEntriesChart(result.data.dailyEntries); } else { showStatus('Failed to load dashboard data', 'error'); @@ -18,14 +23,29 @@ async function loadDashboardData() { } catch (error) { console.error('Dashboard loading error:', error); showStatus('Error loading dashboard', 'error'); + } finally { + setLoadingState(false); } } +// Set loading state +function setLoadingState(isLoading) { + const cards = document.querySelectorAll('.card-value'); + cards.forEach(card => { + if (isLoading) { + card.textContent = '...'; + card.style.opacity = '0.6'; + } else { + card.style.opacity = '1'; + } + }); +} + // Update summary cards function updateDashboardCards(data) { document.getElementById('total-locations').textContent = data.totalLocations.toLocaleString(); document.getElementById('overall-score').textContent = data.overallScore; - document.getElementById('sign-requests').textContent = data.signRequests.toLocaleString(); + document.getElementById('sign-delivered').textContent = data.signDelivered.toLocaleString(); document.getElementById('total-users').textContent = data.totalUsers.toLocaleString(); } @@ -91,6 +111,90 @@ function createSupportLevelChart(supportLevels) { }); } +// Create sign sizes chart +function createSignSizesChart(signSizes) { + const ctx = document.getElementById('sign-sizes-chart'); + if (!ctx) return; + + if (signSizesChart) { + signSizesChart.destroy(); + } + + // Check if mobile + const isMobile = window.innerWidth <= 480; + + signSizesChart = new Chart(ctx, { + type: 'bar', + data: { + labels: ['Regular', 'Large', 'Unsure'], + datasets: [{ + label: 'Signs Requested', + data: [ + signSizes['Regular'] || 0, + signSizes['Large'] || 0, + signSizes['Unsure'] || 0 + ], + backgroundColor: [ + '#2196F3', // Blue for Regular + '#4CAF50', // Green for Large + '#FF9800' // Orange for Unsure + ], + borderColor: [ + '#1976D2', + '#388E3C', + '#F57C00' + ], + borderWidth: 1 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: !isMobile, + position: 'top' + }, + tooltip: { + callbacks: { + label: function(context) { + const label = context.label || ''; + const value = context.parsed.y || 0; + const total = context.dataset.data.reduce((a, b) => a + b, 0); + const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0; + return `${label}: ${value} (${percentage}%)`; + } + } + } + }, + scales: { + y: { + beginAtZero: true, + ticks: { + stepSize: 1, + font: { + size: isMobile ? 10 : 12 + } + }, + grid: { + display: !isMobile + } + }, + x: { + ticks: { + font: { + size: isMobile ? 10 : 12 + } + }, + grid: { + display: false + } + } + } + } + }); +} + // Create daily entries chart function createEntriesChart(dailyEntries) { const ctx = document.getElementById('entries-chart'); @@ -251,6 +355,16 @@ document.addEventListener('DOMContentLoaded', () => { createSupportLevelChart(supportLevels); } + if (signSizesChart) { + const currentData = signSizesChart.data.datasets[0].data; + const signSizes = { + 'Regular': currentData[0] || 0, + 'Large': currentData[1] || 0, + 'Unsure': currentData[2] || 0 + }; + createSignSizesChart(signSizes); + } + if (entriesChart) { const currentLabels = entriesChart.data.labels; const currentData = entriesChart.data.datasets[0].data; diff --git a/map/app/services/nocodb.js b/map/app/services/nocodb.js index 2413dc8..16f7c95 100644 --- a/map/app/services/nocodb.js +++ b/map/app/services/nocodb.js @@ -46,6 +46,53 @@ class NocoDBService { const response = await this.client.get(url, { params }); return response.data; } + + // Get ALL records from a table using pagination + async getAllPaginated(tableId, params = {}) { + try { + let allRecords = []; + let offset = 0; + const limit = params.limit || 100; + let hasMore = true; + + while (hasMore) { + const response = await this.getAll(tableId, { + ...params, + limit: limit, + offset: offset + }); + + const records = response.list || []; + allRecords = allRecords.concat(records); + + // Check if there are more records + hasMore = records.length === limit; + offset += limit; + + // Safety check to prevent infinite loops + if (offset > 10000) { + logger.warn(`Reached maximum offset limit while fetching records from table ${tableId}`); + break; + } + } + + logger.info(`Fetched ${allRecords.length} total records from table ${tableId}`); + + return { + list: allRecords, + pageInfo: { + totalRows: allRecords.length, + page: 1, + pageSize: allRecords.length, + isFirstPage: true, + isLastPage: true + } + }; + } catch (error) { + logger.error('Error fetching paginated records:', error); + throw error; + } + } // Get single record async getById(tableId, recordId) { @@ -77,6 +124,12 @@ class NocoDBService { // Get locations with proper filtering async getLocations(params = {}) { + // For locations, we want all records by default, so use getAllPaginated + // unless specific limit/offset are provided + if (!params.limit && !params.offset) { + return this.getAllPaginated(config.nocodb.tableId, params); + } + const defaultParams = { limit: 1000, offset: 0,