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,