932e5e137c
- Add database tables for email configs, settings, and activity logs - Create backend endpoints for dashboard stats, analytics, archives, email config, and settings - Create frontend service layer (admin, archive, email, settings services) - Update AdminDashboard to use real statistics and activity data - Update AnalyticsPage to fetch real analytics from backend - Update ArchivesPage with pagination and real archive operations - Update EmailConfigPage to manage real SMTP config and templates - Remove all mock data and replace with API calls throughout admin interface 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
218 lines
7.0 KiB
JavaScript
218 lines
7.0 KiB
JavaScript
const express = require('express');
|
|
const { db } = require('../database/db');
|
|
const { adminAuth } = require('../middleware/auth');
|
|
const router = express.Router();
|
|
|
|
// Get dashboard statistics
|
|
router.get('/stats', adminAuth, async (req, res) => {
|
|
try {
|
|
// Get active events count
|
|
const activeEvents = await db('events')
|
|
.where('is_active', true)
|
|
.where('is_archived', false)
|
|
.count('id as count')
|
|
.first();
|
|
|
|
// Get events expiring within 7 days
|
|
const expiringEvents = await db('events')
|
|
.where('is_active', true)
|
|
.where('is_archived', false)
|
|
.whereRaw('expires_at <= datetime("now", "+7 days")')
|
|
.whereRaw('expires_at > datetime("now")')
|
|
.count('id as count')
|
|
.first();
|
|
|
|
// Get total photos count
|
|
const totalPhotos = await db('photos')
|
|
.count('id as count')
|
|
.first();
|
|
|
|
// Get storage usage (sum of all photo sizes)
|
|
const storageUsed = await db('photos')
|
|
.sum('size_bytes as total')
|
|
.first();
|
|
|
|
// Get total views (last 30 days)
|
|
const totalViews = await db('access_logs')
|
|
.where('action', 'view')
|
|
.whereRaw('timestamp >= datetime("now", "-30 days")')
|
|
.count('id as count')
|
|
.first();
|
|
|
|
// Get total downloads (last 30 days)
|
|
const totalDownloads = await db('access_logs')
|
|
.where('action', 'download')
|
|
.whereRaw('timestamp >= datetime("now", "-30 days")')
|
|
.count('id as count')
|
|
.first();
|
|
|
|
// Calculate trends (compare with previous 30 days)
|
|
const previousViews = await db('access_logs')
|
|
.where('action', 'view')
|
|
.whereRaw('timestamp >= datetime("now", "-60 days")')
|
|
.whereRaw('timestamp < datetime("now", "-30 days")')
|
|
.count('id as count')
|
|
.first();
|
|
|
|
const previousDownloads = await db('access_logs')
|
|
.where('action', 'download')
|
|
.whereRaw('timestamp >= datetime("now", "-60 days")')
|
|
.whereRaw('timestamp < datetime("now", "-30 days")')
|
|
.count('id as count')
|
|
.first();
|
|
|
|
// Calculate trend percentages
|
|
const viewsTrend = previousViews.count > 0
|
|
? ((totalViews.count - previousViews.count) / previousViews.count) * 100
|
|
: 0;
|
|
|
|
const downloadsTrend = previousDownloads.count > 0
|
|
? ((totalDownloads.count - previousDownloads.count) / previousDownloads.count) * 100
|
|
: 0;
|
|
|
|
res.json({
|
|
activeEvents: activeEvents.count || 0,
|
|
expiringEvents: expiringEvents.count || 0,
|
|
totalPhotos: totalPhotos.count || 0,
|
|
storageUsed: storageUsed.total || 0,
|
|
totalViews: totalViews.count || 0,
|
|
totalDownloads: totalDownloads.count || 0,
|
|
viewsTrend: Math.round(viewsTrend * 10) / 10,
|
|
downloadsTrend: Math.round(downloadsTrend * 10) / 10
|
|
});
|
|
} catch (error) {
|
|
console.error('Dashboard stats error:', error);
|
|
res.status(500).json({ error: 'Failed to fetch dashboard statistics' });
|
|
}
|
|
});
|
|
|
|
// Get recent activity
|
|
router.get('/activity', adminAuth, async (req, res) => {
|
|
try {
|
|
const limit = parseInt(req.query.limit) || 10;
|
|
|
|
const activities = await db('activity_logs')
|
|
.select('activity_logs.*', 'events.event_name')
|
|
.leftJoin('events', 'activity_logs.event_id', 'events.id')
|
|
.orderBy('activity_logs.created_at', 'desc')
|
|
.limit(limit);
|
|
|
|
// Format activities
|
|
const formattedActivities = activities.map(activity => ({
|
|
id: activity.id,
|
|
type: activity.activity_type,
|
|
actorType: activity.actor_type,
|
|
actorName: activity.actor_name,
|
|
eventName: activity.event_name,
|
|
metadata: activity.metadata ? JSON.parse(activity.metadata) : {},
|
|
createdAt: activity.created_at
|
|
}));
|
|
|
|
res.json(formattedActivities);
|
|
} catch (error) {
|
|
console.error('Activity log error:', error);
|
|
res.status(500).json({ error: 'Failed to fetch activity log' });
|
|
}
|
|
});
|
|
|
|
// Get analytics data for charts
|
|
router.get('/analytics', adminAuth, async (req, res) => {
|
|
try {
|
|
const days = parseInt(req.query.days) || 7;
|
|
|
|
// Generate date range
|
|
const dates = [];
|
|
for (let i = days - 1; i >= 0; i--) {
|
|
dates.push({
|
|
date: new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
|
views: 0,
|
|
downloads: 0,
|
|
uniqueVisitors: 0
|
|
});
|
|
}
|
|
|
|
// Get views per day
|
|
const viewsData = await db('access_logs')
|
|
.select(db.raw('DATE(timestamp) as date'), db.raw('COUNT(*) as count'))
|
|
.where('action', 'view')
|
|
.whereRaw(`timestamp >= datetime("now", "-${days} days")`)
|
|
.groupByRaw('DATE(timestamp)');
|
|
|
|
// Get downloads per day
|
|
const downloadsData = await db('access_logs')
|
|
.select(db.raw('DATE(timestamp) as date'), db.raw('COUNT(*) as count'))
|
|
.where('action', 'download')
|
|
.whereRaw(`timestamp >= datetime("now", "-${days} days")`)
|
|
.groupByRaw('DATE(timestamp)');
|
|
|
|
// Get unique visitors per day
|
|
const visitorsData = await db('access_logs')
|
|
.select(db.raw('DATE(timestamp) as date'), db.raw('COUNT(DISTINCT ip_address) as count'))
|
|
.whereRaw(`timestamp >= datetime("now", "-${days} days")`)
|
|
.groupByRaw('DATE(timestamp)');
|
|
|
|
// Merge data into dates array
|
|
viewsData.forEach(row => {
|
|
const dateObj = dates.find(d => d.date === row.date);
|
|
if (dateObj) dateObj.views = row.count;
|
|
});
|
|
|
|
downloadsData.forEach(row => {
|
|
const dateObj = dates.find(d => d.date === row.date);
|
|
if (dateObj) dateObj.downloads = row.count;
|
|
});
|
|
|
|
visitorsData.forEach(row => {
|
|
const dateObj = dates.find(d => d.date === row.date);
|
|
if (dateObj) dateObj.uniqueVisitors = row.count;
|
|
});
|
|
|
|
// Get top galleries by views
|
|
const topGalleries = await db('access_logs')
|
|
.select('events.event_name', 'events.slug')
|
|
.select(db.raw('COUNT(*) as views'))
|
|
.join('events', 'access_logs.event_id', 'events.id')
|
|
.where('access_logs.action', 'view')
|
|
.whereRaw(`access_logs.timestamp >= datetime("now", "-${days} days")`)
|
|
.groupBy('events.id')
|
|
.orderBy('views', 'desc')
|
|
.limit(5);
|
|
|
|
// Get device breakdown (simplified - based on user agent)
|
|
const deviceData = await db('access_logs')
|
|
.select(
|
|
db.raw(`
|
|
CASE
|
|
WHEN user_agent LIKE '%Mobile%' THEN 'mobile'
|
|
WHEN user_agent LIKE '%Tablet%' OR user_agent LIKE '%iPad%' THEN 'tablet'
|
|
ELSE 'desktop'
|
|
END as device_type
|
|
`),
|
|
db.raw('COUNT(*) as count')
|
|
)
|
|
.whereRaw(`timestamp >= datetime("now", "-${days} days")`)
|
|
.groupBy('device_type');
|
|
|
|
const totalDevices = deviceData.reduce((sum, d) => sum + d.count, 0);
|
|
const devices = {
|
|
desktop: 0,
|
|
mobile: 0,
|
|
tablet: 0
|
|
};
|
|
|
|
deviceData.forEach(d => {
|
|
devices[d.device_type] = Math.round((d.count / totalDevices) * 100);
|
|
});
|
|
|
|
res.json({
|
|
chartData: dates,
|
|
topGalleries,
|
|
devices
|
|
});
|
|
} catch (error) {
|
|
console.error('Analytics error:', error);
|
|
res.status(500).json({ error: 'Failed to fetch analytics data' });
|
|
}
|
|
});
|
|
|
|
module.exports = router; |