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

@@ -47,11 +47,14 @@ export function LitterTypeChart({ data, loading }: LitterTypeChartProps) {
);
}
// Calculate total for percentages
const total = data.reduce((sum, item) => sum + item.count, 0);
// Format data for the chart
const chartData = data.map(item => ({
name: LITTER_TYPE_LABELS[item.type] || item.type,
value: item.count,
percentage: item.percentage,
percentage: total > 0 ? (item.count / total) * 100 : 0,
}));
const renderCustomLabel = (entry: { name: string; percentage: number }) => {

View File

@@ -43,12 +43,15 @@ export function QuantityChart({ data, loading }: QuantityChartProps) {
);
}
// Calculate total for percentages
const total = data.reduce((sum, item) => sum + item.count, 0);
// Format data for the chart
const chartData = data.map(item => ({
name: QUANTITY_LABELS[item.quantity] || item.quantity,
name: QUANTITY_LABELS[item.level] || item.level,
count: item.count,
percentage: item.percentage,
quantity: item.quantity,
percentage: total > 0 ? (item.count / total) * 100 : 0,
quantity: item.level,
}));
return (

View File

@@ -57,17 +57,17 @@ export function TimeSeriesChart({ data, loading }: TimeSeriesChartProps) {
/>
<Line
type="monotone"
dataKey="reports"
dataKey="reportCount"
stroke="#3b82f6"
strokeWidth={2}
name="Reports"
dot={{ fill: '#3b82f6', r: 4 }}
activeDot={{ r: 6 }}
/>
{data.some(d => d.events !== undefined) && (
{data.some(d => d.eventCount !== undefined) && (
<Line
type="monotone"
dataKey="events"
dataKey="eventCount"
stroke="#10b981"
strokeWidth={2}
name="Events"
@@ -75,6 +75,17 @@ export function TimeSeriesChart({ data, loading }: TimeSeriesChartProps) {
activeDot={{ r: 6 }}
/>
)}
{data.some(d => d.litterCollected > 0) && (
<Line
type="monotone"
dataKey="litterCollected"
stroke="#f59e0b"
strokeWidth={2}
name="Litter Collected (kg)"
dot={{ fill: '#f59e0b', r: 4 }}
activeDot={{ r: 6 }}
/>
)}
</LineChart>
</ResponsiveContainer>
</div>