← Tasks
[CP-API-03] Dashboard stats RPC (aggregate voter/team data) (retry 1)
completedcode_genP1
Description
## CP-API-03: Dashboard stats RPC (aggregate voter/team data)
Section: API | Milestone: M3 | Owner: AG Lu
### Failing Tests
- **stats_api_file** (file_exists): File `campaignpro/src/api/stats.ts` must exist
### Acceptance Criteria
- File `campaignpro/src/api/stats.ts` must exist
### Codebase Conventions
- Trilingual comments: // EN: ... // VI: ... // ES: ...
- SPEC section refs in comments
- Zod at module level with .safeParse()
- (supabase as any) with eslint-disable
- NextResponse.json() responses
- Error format: { error: 'message' }
### Dependencies
- CP-API-01: FAIL
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
1d ago
Completed
1d ago
Quality
Overall
99%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"cost": {
"input_tokens": 223,
"estimated_usd": 0.036579,
"output_tokens": 2394
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 29939,
"response_text": "I'll create the dashboard stats RPC API file that aggregates voter and team data.\n\n```typescript\n// campaignpro/src/api/stats.ts\n// EN: Dashboard statistics API for aggregating voter and team data\n// VI: API thống kê dashboard để tổng hợp dữ liệu cử tri và nhóm\n// ES: API de estadísticas del dashboard para agregar datos de votantes y equipos\n// SPEC: CP-API-03 - Dashboard stats RPC (aggregate voter/team data)\n\nimport { z } from 'zod';\nimport { createClient } from '@supabase/supabase-js';\nimport { NextRequest, NextResponse } from 'next/server';\n\n// EN: Zod schemas for request/response validation\n// VI: Schemas Zod để xác thực request/response\n// ES: Esquemas Zod para validación de request/response\nconst StatsRequestSchema = z.object({\n campaignId: z.string().uuid(),\n timeRange: z.enum(['7d', '30d', '90d', 'all']).optional().default('30d'),\n teamId: z.string().uuid().optional(),\n});\n\nconst StatsResponseSchema = z.object({\n voterStats: z.object({\n total: z.number(),\n active: z.number(),\n newThisMonth: z.number(),\n engagement: z.number(),\n }),\n teamStats: z.object({\n totalTeams: z.number(),\n activeMembers: z.number(),\n topPerformers: z.array(z.object({\n teamId: z.string(),\n teamName: z.string(),\n memberCount: z.number(),\n engagementScore: z.number(),\n })),\n }),\n trends: z.object({\n voterGrowth: z.array(z.object({\n date: z.string(),\n count: z.number(),\n })),\n engagementTrend: z.array(z.object({\n date: z.string(),\n score: z.number(),\n })),\n }),\n});\n\n// EN: Main stats API handler\n// VI: Handler API thống kê chính\n// ES: Manejador principal de API de estadísticas\nexport async function POST(request: NextRequest) {\n try {\n const body = await request.json();\n const validation = StatsRequestSchema.safeParse(body);\n \n if (!validation.success) {\n return NextResponse.json(\n { error: 'Invalid request parameters' },\n { status: 400 }\n );\n }\n\n const { campaignId, timeRange, teamId } = validation.data;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const supabase = createClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.SUPABASE_SERVICE_ROLE_KEY!\n ) as any;\n\n // EN: Get aggregated voter statistics\n // VI: Lấy thống kê cử tri tổng hợp\n // ES: Obtener estadísticas agregadas de votantes\n const voterStats = await getVoterStats(supabase, campaignId, timeRange);\n\n // EN: Get team performance statistics\n // VI: Lấy thống kê hiệu suất nhóm\n // ES: Obtener estadísticas de rendimiento del equipo\n const teamStats = await getTeamStats(supabase, campaignId, teamId);\n\n // EN: Get trend data for charts\n // VI: Lấy dữ liệu xu hướng cho biểu đồ\n // ES: Obtener datos de tendencias para gráficos\n const trends = await getTrendData(supabase, campaignId, timeRange);\n\n const response = {\n voterStats,\n teamStats,\n trends,\n };\n\n const validatedResponse = StatsResponseSchema.safeParse(response);\n if (!validatedResponse.success) {\n return NextResponse.json(\n { error: 'Invalid response data' },\n { status: 500 }\n );\n }\n\n return NextResponse.json(validatedResponse.data);\n\n } catch (error) {\n console.error('Stats API error:', error);\n return NextResponse.json(\n { error: 'Failed to fetch statistics' },\n { status: 500 }\n );\n }\n}\n\n// EN: Get voter statistics using RPC functions\n// VI: Lấy thống kê cử tri sử dụng hàm RPC\n// ES: Obtener estadísticas de votantes usando funciones RPC\nasync function getVoterStats(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n supabase: any,\n campaignId: string,\n timeRange: string\n) {\n const { data, error } = await supabase.rpc('get_voter_stats', {\n p_campaign_id: campaignId,\n p_time_range: timeRange,\n });\n\n if (error) throw error;\n\n return {\n total: data?.total_voters || 0,\n active: data?.active_voters || 0,\n newThisMonth: data?.new_voters || 0,\n engagement: data?.avg_engagement || 0,\n };\n}\n\n// EN: Get team statistics and performance data\n// VI: Lấy thống kê và dữ liệu hiệu suất nhóm\n// ES: Obtener estadísticas y datos de rendimiento del equipo\nasync function getTeamStats(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n supabase: any,\n campaignId: string,\n teamId?: string\n) {\n const { data, error } = await supabase.rpc('get_team_stats', {\n p_campaign_id: campaignId,\n p_team_id: teamId || null,\n });\n\n if (error) throw error;\n\n return {\n totalTeams: data?.total_teams || 0,\n activeMembers: data?.active_members || 0,\n topPerformers: data?.top_performers || [],\n };\n}\n\n// EN: Get trend data for dashboard charts\n// VI: Lấy dữ liệu xu hướng cho biểu đồ dashboard\n// ES: Obtener datos de tendencias para gráficos del dashboard\nasync function getTrendData(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n supabase: any,\n campaignId: string,\n timeRange: string\n) {\n const { data: voterTrend, error: voterError } = await supabase.rpc('get_voter_growth_trend', {\n p_campaign_id: campaignId,\n p_time_range: timeRange,\n });\n\n const { data: engagementTrend, error: engagementError } = await supabase.rpc('get_engagement_trend', {\n p_campaign_id: campaignId,\n p_time_range: timeRange,\n });\n\n if (voterError || engagementError) {\n throw new Error('Failed to fetch trend data');\n }\n\n return {\n voterGrowth: voterTrend || [],\n engagementTrend: engagementTrend || [],\n };\n}\n\n// EN: GET handler for fetching stats without request body\n// VI: Handler GET để lấy thống kê không cần request body\n// ES: Manejador GET para obtener estadísticas sin cuerpo de solicitud\nexport async function GET(request: NextRequest) {\n const { searchParams } = new URL(request.url);\n const campaignId = searchParams.get('campaignId');\n const timeRange = searchParams.get('timeRange') || '30d';\n const teamId = searchParams.get('teamId');\n\n if (!campaignId) {\n return NextResponse.json(\n { error: 'Campaign ID is required' },\n { status: 400 }\n );\n }\n\n // EN: Reuse POST logic with URL parameters\n // VI: Tái sử dụng logic POST với tham số URL\n // ES: Reutilizar lógica POST con parámetros de URL\n const mockRequest = new Request('http://localhost', {\n method: 'POST',\n body: JSON.stringify({\n campaignId,\n timeRange,\n ...(teamId && { teamId }),\n }),\n });\n\n return POST(mockRequest as NextRequest);\n}\n```\n\nThis implementation provides:\n\n1. **Dashboard Statistics API** that aggregates voter and team data\n2. **Trilingual comments** in English, Vietnamese, and Spanish\n3. **Zod validation** with module-level schemas and `.safeParse()`\n4. **Supabase RPC calls** with proper type casting and eslint-disable\n5. **NextResponse.json()** for all responses\n6. **Error handling** with consistent `{ error: 'message' }` format\n7. **Both POST and GET** handlers for flexibility\n8. **Comprehensive data aggregation** including:\n - Voter statistics (total, active, new, engagement)\n - Team performance metrics\n - Trend data for dashboard charts\n9. **SPEC reference** to CP-API-03 in comments\n\nThe file satisfies the acceptance criteria by existing at `campaignpro/src/api/stats.ts` and follows all the codebase conventions specified."
}Task ID: 183216e5-2b1b-4f88-9118-8bd6ed4ae528