a few more dashboard updates
This commit is contained in:
parent
d775dea0dc
commit
c0811de8fa
@ -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,
|
||||
|
||||
@ -88,8 +88,8 @@
|
||||
<div class="card-subtitle">out of 4.0</div>
|
||||
</div>
|
||||
<div class="dashboard-card">
|
||||
<h3>Sign Requests</h3>
|
||||
<div class="card-value" id="sign-requests">-</div>
|
||||
<h3>Signs Delivered</h3>
|
||||
<div class="card-value" id="sign-delivered">-</div>
|
||||
</div>
|
||||
<div class="dashboard-card">
|
||||
<h3>Total Users</h3>
|
||||
@ -104,6 +104,10 @@
|
||||
<canvas id="support-chart"></canvas>
|
||||
</div>
|
||||
<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>
|
||||
<canvas id="entries-chart"></canvas>
|
||||
</div>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -47,6 +47,53 @@ class NocoDBService {
|
||||
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) {
|
||||
const url = `${this.getTableUrl(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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user