anaylitics dashboard
This commit is contained in:
parent
7edc66565c
commit
373018cebb
@ -296,6 +296,14 @@ Users with admin privileges can access the admin panel at `/admin.html` to confi
|
||||
|
||||
### Features
|
||||
|
||||
#### Dashboard Analytics
|
||||
|
||||
- **Campaign Overview**: Real-time statistics and metrics
|
||||
- **Support Distribution**: Visual breakdown of support levels (1-4)
|
||||
- **Sign Tracking**: Monitor lawn sign requests
|
||||
- **User Analytics**: Track user growth and daily entries
|
||||
- **Performance Score**: Overall campaign performance metric
|
||||
|
||||
#### Start Location Configuration
|
||||
|
||||
- **Interactive Map**: Visual interface for selecting coordinates
|
||||
|
||||
70
map/app/controllers/dashboardController.js
Normal file
70
map/app/controllers/dashboardController.js
Normal file
@ -0,0 +1,70 @@
|
||||
const nocodbService = require('../services/nocodb');
|
||||
const logger = require('../utils/logger');
|
||||
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);
|
||||
const locations = locationsResponse.list || [];
|
||||
|
||||
// Calculate support level distribution
|
||||
const supportLevels = { '1': 0, '2': 0, '3': 0, '4': 0 };
|
||||
let signRequests = 0;
|
||||
|
||||
locations.forEach(loc => {
|
||||
if (loc['Support Level']) {
|
||||
supportLevels[loc['Support Level']]++;
|
||||
}
|
||||
if (loc.Sign || loc.sign) {
|
||||
signRequests++;
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate overall score (weighted average)
|
||||
const totalResponses = Object.values(supportLevels).reduce((a, b) => a + b, 0);
|
||||
const weightedScore = (supportLevels['1'] * 4 + supportLevels['2'] * 3 +
|
||||
supportLevels['3'] * 2 + supportLevels['4'] * 1) /
|
||||
(totalResponses || 1);
|
||||
|
||||
// Get user stats
|
||||
const usersResponse = await nocodbService.getAll(config.nocodb.loginSheetId);
|
||||
const users = usersResponse.list || [];
|
||||
|
||||
// Get daily entry counts for the last 30 days
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
const dailyEntries = {};
|
||||
locations.forEach(loc => {
|
||||
const createdAt = new Date(loc.CreatedAt || loc.created_at);
|
||||
if (createdAt >= thirtyDaysAgo) {
|
||||
const dateKey = createdAt.toISOString().split('T')[0];
|
||||
dailyEntries[dateKey] = (dailyEntries[dateKey] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
supportLevels,
|
||||
signRequests,
|
||||
totalLocations: locations.length,
|
||||
overallScore: weightedScore.toFixed(2),
|
||||
totalUsers: users.length,
|
||||
dailyEntries
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error fetching dashboard stats:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to fetch dashboard statistics'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new DashboardController();
|
||||
@ -43,6 +43,10 @@
|
||||
<button id="close-sidebar" class="close-sidebar">×</button>
|
||||
</div>
|
||||
<nav class="admin-nav">
|
||||
<a href="#dashboard">
|
||||
<span class="nav-icon">📊</span>
|
||||
<span class="nav-text">Dashboard</span>
|
||||
</a>
|
||||
<a href="#start-location" class="active">
|
||||
<span class="nav-icon">📍</span>
|
||||
<span class="nav-text">Start Location</span>
|
||||
@ -66,6 +70,47 @@
|
||||
</div>
|
||||
|
||||
<div class="admin-content">
|
||||
<!-- Dashboard Section -->
|
||||
<section id="dashboard" class="admin-section" style="display: none;">
|
||||
<h2>Campaign Dashboard</h2>
|
||||
<p>Overview of campaign metrics and statistics</p>
|
||||
|
||||
<div class="dashboard-container">
|
||||
<!-- Summary Cards -->
|
||||
<div class="dashboard-cards">
|
||||
<div class="dashboard-card">
|
||||
<h3>Total Locations</h3>
|
||||
<div class="card-value" id="total-locations">-</div>
|
||||
</div>
|
||||
<div class="dashboard-card">
|
||||
<h3>Overall Score</h3>
|
||||
<div class="card-value" id="overall-score">-</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="dashboard-card">
|
||||
<h3>Total Users</h3>
|
||||
<div class="card-value" id="total-users">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts -->
|
||||
<div class="dashboard-charts">
|
||||
<div class="chart-container">
|
||||
<h3>Support Level Distribution</h3>
|
||||
<canvas id="support-chart"></canvas>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<h3>Daily Entries (Last 30 Days)</h3>
|
||||
<canvas id="entries-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Start Location Section -->
|
||||
<section id="start-location" class="admin-section">
|
||||
<h2>Map Start Location</h2>
|
||||
@ -372,6 +417,12 @@
|
||||
<!-- Cache Management -->
|
||||
<script src="js/cache-manager.js"></script>
|
||||
|
||||
<!-- Chart.js library -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
|
||||
<!-- Dashboard JavaScript -->
|
||||
<script src="js/dashboard.js"></script>
|
||||
|
||||
<!-- Admin JavaScript -->
|
||||
<script src="js/admin.js"></script>
|
||||
</body>
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
/* Admin Panel Specific Styles */
|
||||
@import url("modules/dashboard.css");
|
||||
|
||||
.admin-container {
|
||||
display: flex;
|
||||
height: calc(100vh - var(--header-height));
|
||||
|
||||
85
map/app/public/css/modules/dashboard.css
Normal file
85
map/app/public/css/modules/dashboard.css
Normal file
@ -0,0 +1,85 @@
|
||||
/* Dashboard Styles */
|
||||
.dashboard-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.dashboard-card h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1rem;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: #999;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.dashboard-charts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.chart-container h3 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
font-size: 1.25rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.chart-container canvas {
|
||||
height: 300px !important;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-cards {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.dashboard-charts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.dashboard-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
150
map/app/public/js/dashboard.js
Normal file
150
map/app/public/js/dashboard.js
Normal file
@ -0,0 +1,150 @@
|
||||
// Dashboard functionality
|
||||
let supportChart = null;
|
||||
let entriesChart = null;
|
||||
|
||||
// Load dashboard data
|
||||
async function loadDashboardData() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/dashboard/stats');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
updateDashboardCards(result.data);
|
||||
createSupportLevelChart(result.data.supportLevels);
|
||||
createEntriesChart(result.data.dailyEntries);
|
||||
} else {
|
||||
showStatus('Failed to load dashboard data', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Dashboard loading error:', error);
|
||||
showStatus('Error loading dashboard', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 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('total-users').textContent = data.totalUsers.toLocaleString();
|
||||
}
|
||||
|
||||
// Create support level distribution chart
|
||||
function createSupportLevelChart(supportLevels) {
|
||||
const ctx = document.getElementById('support-chart');
|
||||
if (!ctx) return;
|
||||
|
||||
if (supportChart) {
|
||||
supportChart.destroy();
|
||||
}
|
||||
|
||||
supportChart = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Strong Support (1)', 'Support (2)', 'Neutral (3)', 'Opposed (4)'],
|
||||
datasets: [{
|
||||
data: [
|
||||
supportLevels['1'] || 0,
|
||||
supportLevels['2'] || 0,
|
||||
supportLevels['3'] || 0,
|
||||
supportLevels['4'] || 0
|
||||
],
|
||||
backgroundColor: [
|
||||
'#4CAF50',
|
||||
'#FFC107',
|
||||
'#FF9800',
|
||||
'#F44336'
|
||||
]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create daily entries chart
|
||||
function createEntriesChart(dailyEntries) {
|
||||
const ctx = document.getElementById('entries-chart');
|
||||
if (!ctx) return;
|
||||
|
||||
if (entriesChart) {
|
||||
entriesChart.destroy();
|
||||
}
|
||||
|
||||
// Generate last 30 days
|
||||
const labels = [];
|
||||
const data = [];
|
||||
const today = new Date();
|
||||
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const date = new Date(today);
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateKey = date.toISOString().split('T')[0];
|
||||
labels.push(date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }));
|
||||
data.push(dailyEntries[dateKey] || 0);
|
||||
}
|
||||
|
||||
entriesChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'New Entries',
|
||||
data: data,
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
stepSize: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listener for dashboard navigation
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Update navigation to load dashboard when clicked
|
||||
const dashboardLink = document.querySelector('.admin-nav a[href="#dashboard"]');
|
||||
if (dashboardLink) {
|
||||
dashboardLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Hide all sections
|
||||
document.querySelectorAll('.admin-section').forEach(section => {
|
||||
section.style.display = 'none';
|
||||
});
|
||||
|
||||
// Show dashboard
|
||||
const dashboardSection = document.getElementById('dashboard');
|
||||
if (dashboardSection) {
|
||||
dashboardSection.style.display = 'block';
|
||||
}
|
||||
|
||||
// Update active nav
|
||||
document.querySelectorAll('.admin-nav a').forEach(link => {
|
||||
link.classList.remove('active');
|
||||
});
|
||||
dashboardLink.classList.add('active');
|
||||
|
||||
// Load dashboard data
|
||||
loadDashboardData();
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -1,6 +1,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const settingsController = require('../controllers/settingsController');
|
||||
const dashboardRoutes = require('./dashboard');
|
||||
|
||||
// Debug endpoint to check configuration
|
||||
router.get('/config-debug', (req, res) => {
|
||||
@ -30,4 +31,7 @@ router.post('/start-location', settingsController.updateStartLocation);
|
||||
router.get('/walk-sheet-config', settingsController.getWalkSheetConfig);
|
||||
router.post('/walk-sheet-config', settingsController.updateWalkSheetConfig);
|
||||
|
||||
// Dashboard routes
|
||||
router.use('/dashboard', dashboardRoutes);
|
||||
|
||||
module.exports = router;
|
||||
7
map/app/routes/dashboard.js
Normal file
7
map/app/routes/dashboard.js
Normal file
@ -0,0 +1,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const dashboardController = require('../controllers/dashboardController');
|
||||
|
||||
router.get('/stats', dashboardController.getStats);
|
||||
|
||||
module.exports = router;
|
||||
@ -46,6 +46,10 @@ Controller for application settings, start location, and walk sheet config.
|
||||
|
||||
Controller for volunteer shift management (public and admin). When users sign up for shifts, the signup record includes the shift title for better tracking and display. Fixed variable scope issue in signup method to prevent "allSignups is not defined" error.
|
||||
|
||||
# app/controllers/dashboardController.js
|
||||
|
||||
Controller for aggregating and calculating dashboard statistics from locations and users data.
|
||||
|
||||
# app/controllers/usersController.js
|
||||
|
||||
Controller for user management (list, create, delete users).
|
||||
@ -114,6 +118,10 @@ Defines styles for all button types, states (hover, disabled), and variants (pri
|
||||
|
||||
Styles for the cache busting update notification that prompts users to refresh the page.
|
||||
|
||||
# app/public/css/modules/dashboard.css
|
||||
|
||||
Styles for the dashboard panel including cards, charts, and responsive grid layouts.
|
||||
|
||||
# app/public/css/modules/doc-search.css
|
||||
|
||||
Styles for the documentation search component in the header.
|
||||
@ -198,6 +206,10 @@ CSS styles for the user profile page and user management components in the admin
|
||||
|
||||
JavaScript for admin panel functionality (map, start location, walk sheet, shift management, and user management).
|
||||
|
||||
# app/public/js/dashboard.js
|
||||
|
||||
JavaScript for rendering dashboard charts and statistics using Chart.js in the admin panel.
|
||||
|
||||
# app/public/js/user.js
|
||||
|
||||
JavaScript for user profile page functionality and user account management.
|
||||
@ -242,6 +254,10 @@ Utility functions for the frontend (escaping HTML, parsing geolocation, etc).
|
||||
|
||||
Express router for admin-only endpoints (start location, walk sheet config).
|
||||
|
||||
# app/routes/dashboard.js
|
||||
|
||||
Express router for dashboard API endpoints, providing stats for the admin dashboard.
|
||||
|
||||
# app/routes/auth.js
|
||||
|
||||
Express router for authentication endpoints (login, logout, check).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user