feat: implement analytics endpoints (task 28)

- Created analytics service with summary, trends, comparison, and aggregated data methods
- Implemented 5 analytics endpoints: summary, trends, comparison, aggregated, and CSV export
- Added time series data with day/week/month interval support
- Implemented period-over-period comparison metrics with percentage changes
- Added CSV export functionality using json2csv
- Updated frontend analytics service to unwrap API responses
- Fixed chart components to work with new data structure (reportCount, eventCount, litterCollected)
- Updated TypeScript types to match backend API structure
- All endpoints tested and verified working

Requirements satisfied: 5.1, 5.2, 5.3, 5.4, 5.5
This commit is contained in:
2025-11-17 00:13:07 +00:00
parent ea08bf4289
commit d91a13da60
16 changed files with 1051 additions and 78 deletions

View File

@@ -46,6 +46,13 @@ export function AnalyticsPage() {
totalEvents: 0,
totalUsers: 0,
totalLitterCollected: 0,
reportsByType: {},
reportsByQuantity: {},
verifiedReports: 0,
cleanedReports: 0,
activeUsers: 0,
upcomingEvents: 0,
completedEvents: 0,
});
setError('Analytics API not yet implemented. Showing placeholder data.');
} finally {
@@ -85,29 +92,30 @@ export function AnalyticsPage() {
date.setDate(date.getDate() + Math.floor(i * daysDiff / dataPoints));
mockTimeSeries.push({
date: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
reports: Math.floor(Math.random() * 20) + 5,
events: Math.floor(Math.random() * 5),
reportCount: Math.floor(Math.random() * 20) + 5,
eventCount: Math.floor(Math.random() * 5),
litterCollected: Math.floor(Math.random() * 50) + 10,
});
}
setTimeSeriesData(mockTimeSeries);
// Generate mock litter type distribution
const mockLitterTypes: LitterTypeDistribution[] = [
{ type: 'plastic', count: 45, percentage: 45 },
{ type: 'metal', count: 15, percentage: 15 },
{ type: 'glass', count: 12, percentage: 12 },
{ type: 'organic', count: 10, percentage: 10 },
{ type: 'hazardous', count: 8, percentage: 8 },
{ type: 'other', count: 10, percentage: 10 },
{ type: 'plastic', count: 45 },
{ type: 'metal', count: 15 },
{ type: 'glass', count: 12 },
{ type: 'organic', count: 10 },
{ type: 'hazardous', count: 8 },
{ type: 'other', count: 10 },
];
setLitterTypeData(mockLitterTypes);
// Generate mock quantity aggregation
const mockQuantities: QuantityAggregation[] = [
{ quantity: 'minimal', count: 30, percentage: 30 },
{ quantity: 'moderate', count: 40, percentage: 40 },
{ quantity: 'significant', count: 20, percentage: 20 },
{ quantity: 'severe', count: 10, percentage: 10 },
{ level: 'minimal', count: 30 },
{ level: 'moderate', count: 40 },
{ level: 'significant', count: 20 },
{ level: 'severe', count: 10 },
];
setQuantityData(mockQuantities);
} finally {
@@ -211,14 +219,6 @@ export function AnalyticsPage() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
}
trend={
summary.periodComparison
? {
value: summary.periodComparison.reports,
isPositive: summary.periodComparison.reports > 0,
}
: undefined
}
subtitle="Litter reports submitted"
/>
@@ -230,33 +230,17 @@ export function AnalyticsPage() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
}
trend={
summary.periodComparison
? {
value: summary.periodComparison.events,
isPositive: summary.periodComparison.events > 0,
}
: undefined
}
subtitle="Cleanup events organized"
/>
<StatCard
title="Active Users"
value={summary.totalUsers.toLocaleString()}
value={summary.activeUsers.toLocaleString()}
icon={
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
}
trend={
summary.periodComparison
? {
value: summary.periodComparison.users,
isPositive: summary.periodComparison.users > 0,
}
: undefined
}
subtitle="Contributing to the platform"
/>