Files
ocean-cleanup/src/services/analyticsService.ts
Paul Bailey d91a13da60 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
2025-11-17 00:13:07 +00:00

174 lines
5.4 KiB
TypeScript

// Analytics service for API interactions
import { apiClient } from './api';
import type { AnalyticsSummary, TimeSeriesDataPoint, LitterTypeDistribution, QuantityAggregation, Hotspot, HotspotWithSeverity, HotspotSeverity } from '@/types';
class AnalyticsService {
/**
* Get summary statistics for the analytics dashboard
*/
async getSummary(dateRange?: { start: string; end: string }): Promise<AnalyticsSummary> {
const params = new URLSearchParams();
if (dateRange) {
params.append('startDate', dateRange.start);
params.append('endDate', dateRange.end);
}
const queryString = params.toString();
const response = await apiClient.get<{ success: boolean; data: AnalyticsSummary }>(`/analytics/summary${queryString ? `?${queryString}` : ''}`);
return response.data;
}
/**
* Get time series data for report trends
*/
async getTimeSeries(
dateRange?: { start: string; end: string },
interval: 'day' | 'week' | 'month' = 'day'
): Promise<TimeSeriesDataPoint[]> {
const params = new URLSearchParams();
if (dateRange) {
params.append('startDate', dateRange.start);
params.append('endDate', dateRange.end);
}
params.append('interval', interval);
const queryString = params.toString();
const response = await apiClient.get<{ success: boolean; data: TimeSeriesDataPoint[] }>(`/analytics/trends${queryString ? `?${queryString}` : ''}`);
return response.data;
}
/**
* Get aggregated data (litter types, quantities, verification status)
*/
async getAggregatedData(dateRange?: { start: string; end: string }): Promise<{
litterTypes: LitterTypeDistribution[];
quantities: QuantityAggregation[];
verificationStatus: Array<{ status: string; count: number }>;
}> {
const params = new URLSearchParams();
if (dateRange) {
params.append('startDate', dateRange.start);
params.append('endDate', dateRange.end);
}
const queryString = params.toString();
const response = await apiClient.get<{ success: boolean; data: {
litterTypes: LitterTypeDistribution[];
quantities: QuantityAggregation[];
verificationStatus: Array<{ status: string; count: number }>;
} }>(`/analytics/aggregated${queryString ? `?${queryString}` : ''}`);
return response.data;
}
/**
* Get litter type distribution data
*/
async getLitterTypeDistribution(dateRange?: { start: string; end: string }): Promise<LitterTypeDistribution[]> {
const data = await this.getAggregatedData(dateRange);
return data.litterTypes;
}
/**
* Get quantity estimation aggregation data
*/
async getQuantityAggregation(dateRange?: { start: string; end: string }): Promise<QuantityAggregation[]> {
const data = await this.getAggregatedData(dateRange);
return data.quantities;
}
/**
* Get comparison metrics for period-over-period analysis
*/
async getComparisonMetrics(
currentStart: string,
currentEnd: string,
previousStart: string,
previousEnd: string
): Promise<{
current: { reports: number; events: number; users: number };
previous: { reports: number; events: number; users: number };
percentageChange: { reports: number; events: number; users: number };
}> {
const params = new URLSearchParams({
currentStart,
currentEnd,
previousStart,
previousEnd,
});
const response = await apiClient.get<{ success: boolean; data: {
current: { reports: number; events: number; users: number };
previous: { reports: number; events: number; users: number };
percentageChange: { reports: number; events: number; users: number };
} }>(`/analytics/comparison?${params.toString()}`);
return response.data;
}
/**
* Export analytics data to CSV
*/
async exportToCSV(dateRange?: { start: string; end: string }): Promise<Blob> {
const params = new URLSearchParams();
if (dateRange) {
params.append('startDate', dateRange.start);
params.append('endDate', dateRange.end);
}
const queryString = params.toString();
const response = await fetch(
`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api'}/analytics/export${queryString ? `?${queryString}` : ''}`,
{
method: 'GET',
headers: {
'Accept': 'text/csv',
},
}
);
if (!response.ok) {
throw new Error('Failed to export data');
}
return response.blob();
}
/**
* Get hotspot data for map visualization
*/
async getHotspots(): Promise<HotspotWithSeverity[]> {
const hotspots = await apiClient.get<Hotspot[]>('/analytics/hotspots');
// Calculate severity based on score and add to each hotspot
return hotspots.map(hotspot => ({
...hotspot,
severity: this.calculateHotspotSeverity(hotspot.severityScore, hotspot.reportCount),
}));
}
/**
* Calculate hotspot severity based on score and report count
*/
private calculateHotspotSeverity(score: number, reportCount: number): HotspotSeverity {
// Critical: score >= 0.8 or 20+ reports
if (score >= 0.8 || reportCount >= 20) {
return 'critical';
}
// High: score >= 0.6 or 15+ reports
if (score >= 0.6 || reportCount >= 15) {
return 'high';
}
// Medium: score >= 0.4 or 10+ reports
if (score >= 0.4 || reportCount >= 10) {
return 'medium';
}
// Low: everything else
return 'low';
}
}
export const analyticsService = new AnalyticsService();