a few more dashboard updates

This commit is contained in:
admin 2025-07-31 10:59:22 -06:00
parent d775dea0dc
commit c0811de8fa
5 changed files with 222 additions and 13 deletions

View File

@ -5,20 +5,36 @@ const config = require('../config');
class DashboardController { class DashboardController {
async getStats(req, res) { async getStats(req, res) {
try { try {
// Get all locations for support level stats // Get all locations using the paginated method
const locationsResponse = await nocodbService.getAll(config.nocodb.tableId); const locationsResponse = await nocodbService.getAllPaginated(config.nocodb.tableId);
const locations = locationsResponse.list || []; const locations = locationsResponse.list || [];
logger.info(`Processing ${locations.length} locations for dashboard stats`);
// Calculate support level distribution // Calculate support level distribution
const supportLevels = { '1': 0, '2': 0, '3': 0, '4': 0 }; 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 => { locations.forEach(loc => {
// Support levels
if (loc['Support Level']) { if (loc['Support Level']) {
supportLevels[loc['Support Level']]++; supportLevels[loc['Support Level']]++;
} }
// Signs delivered (where Sign checkbox is checked)
if (loc.Sign || loc.sign) { 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) / supportLevels['3'] * 2 + supportLevels['4'] * 1) /
(totalResponses || 1); (totalResponses || 1);
// Get user stats // Get all users using the paginated method
const usersResponse = await nocodbService.getAll(config.nocodb.loginSheetId); let users = [];
const users = usersResponse.list || []; 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 // Get daily entry counts for the last 30 days
const thirtyDaysAgo = new Date(); const thirtyDaysAgo = new Date();
@ -38,8 +60,8 @@ class DashboardController {
const dailyEntries = {}; const dailyEntries = {};
locations.forEach(loc => { locations.forEach(loc => {
const createdAt = new Date(loc.CreatedAt || loc.created_at); const createdAt = new Date(loc.CreatedAt || loc.created_at || loc.createdAt);
if (createdAt >= thirtyDaysAgo) { if (!isNaN(createdAt.getTime()) && createdAt >= thirtyDaysAgo) {
const dateKey = createdAt.toISOString().split('T')[0]; const dateKey = createdAt.toISOString().split('T')[0];
dailyEntries[dateKey] = (dailyEntries[dateKey] || 0) + 1; dailyEntries[dateKey] = (dailyEntries[dateKey] || 0) + 1;
} }
@ -49,7 +71,8 @@ class DashboardController {
success: true, success: true,
data: { data: {
supportLevels, supportLevels,
signRequests, signDelivered,
signSizes,
totalLocations: locations.length, totalLocations: locations.length,
overallScore: weightedScore.toFixed(2), overallScore: weightedScore.toFixed(2),
totalUsers: users.length, totalUsers: users.length,

View File

@ -88,8 +88,8 @@
<div class="card-subtitle">out of 4.0</div> <div class="card-subtitle">out of 4.0</div>
</div> </div>
<div class="dashboard-card"> <div class="dashboard-card">
<h3>Sign Requests</h3> <h3>Signs Delivered</h3>
<div class="card-value" id="sign-requests">-</div> <div class="card-value" id="sign-delivered">-</div>
</div> </div>
<div class="dashboard-card"> <div class="dashboard-card">
<h3>Total Users</h3> <h3>Total Users</h3>
@ -104,6 +104,10 @@
<canvas id="support-chart"></canvas> <canvas id="support-chart"></canvas>
</div> </div>
<div class="chart-container"> <div class="chart-container">
<h3>Sign Sizes Requested</h3>
<canvas id="sign-sizes-chart"></canvas>
</div>
<div class="chart-container chart-full-width">
<h3>Daily Entries (Last 30 Days)</h3> <h3>Daily Entries (Last 30 Days)</h3>
<canvas id="entries-chart"></canvas> <canvas id="entries-chart"></canvas>
</div> </div>

View File

@ -55,6 +55,10 @@
min-height: 350px; min-height: 350px;
} }
.chart-container.chart-full-width {
grid-column: 1 / -1;
}
.chart-container h3 { .chart-container h3 {
margin: 0 0 1.5rem 0; margin: 0 0 1.5rem 0;
font-size: 1.25rem; font-size: 1.25rem;
@ -68,6 +72,17 @@
touch-action: pan-y; /* Allow vertical scrolling on mobile */ 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 */ /* Responsive design */
@media (max-width: 768px) { @media (max-width: 768px) {
.dashboard-container { .dashboard-container {

View File

@ -1,16 +1,21 @@
// Dashboard functionality // Dashboard functionality
let supportChart = null; let supportChart = null;
let entriesChart = null; let entriesChart = null;
let signSizesChart = null;
// Load dashboard data // Load dashboard data
async function loadDashboardData() { async function loadDashboardData() {
try { try {
// Show loading state
setLoadingState(true);
const response = await fetch('/api/admin/dashboard/stats'); const response = await fetch('/api/admin/dashboard/stats');
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
updateDashboardCards(result.data); updateDashboardCards(result.data);
createSupportLevelChart(result.data.supportLevels); createSupportLevelChart(result.data.supportLevels);
createSignSizesChart(result.data.signSizes);
createEntriesChart(result.data.dailyEntries); createEntriesChart(result.data.dailyEntries);
} else { } else {
showStatus('Failed to load dashboard data', 'error'); showStatus('Failed to load dashboard data', 'error');
@ -18,14 +23,29 @@ async function loadDashboardData() {
} catch (error) { } catch (error) {
console.error('Dashboard loading error:', error); console.error('Dashboard loading error:', error);
showStatus('Error loading dashboard', '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 // Update summary cards
function updateDashboardCards(data) { function updateDashboardCards(data) {
document.getElementById('total-locations').textContent = data.totalLocations.toLocaleString(); document.getElementById('total-locations').textContent = data.totalLocations.toLocaleString();
document.getElementById('overall-score').textContent = data.overallScore; 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(); 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 // Create daily entries chart
function createEntriesChart(dailyEntries) { function createEntriesChart(dailyEntries) {
const ctx = document.getElementById('entries-chart'); const ctx = document.getElementById('entries-chart');
@ -251,6 +355,16 @@ document.addEventListener('DOMContentLoaded', () => {
createSupportLevelChart(supportLevels); 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) { if (entriesChart) {
const currentLabels = entriesChart.data.labels; const currentLabels = entriesChart.data.labels;
const currentData = entriesChart.data.datasets[0].data; const currentData = entriesChart.data.datasets[0].data;

View File

@ -46,6 +46,53 @@ class NocoDBService {
const response = await this.client.get(url, { params }); const response = await this.client.get(url, { params });
return response.data; 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 // Get single record
async getById(tableId, recordId) { async getById(tableId, recordId) {
@ -77,6 +124,12 @@ class NocoDBService {
// Get locations with proper filtering // Get locations with proper filtering
async getLocations(params = {}) { 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 = { const defaultParams = {
limit: 1000, limit: 1000,
offset: 0, offset: 0,