← 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
- **stats_sentiment** (grep): Pattern `sentiment.*count|voter_stats|getDashboardStats` in `campaignpro/src/api/stats.ts` (need 1+ matches)
- **stats_activity** (grep): Pattern `recent_activity|getRecentActivity` in `campaignpro/src/api/stats.ts` (need 1+ matches)
### Acceptance Criteria
- File `campaignpro/src/api/stats.ts` must exist
- File matching `campaignpro/src/api/stats.ts` must contain pattern `sentiment.*count|voter_stats|getDashboardStats`
- File matching `campaignpro/src/api/stats.ts` must contain pattern `recent_activity|getRecentActivity`
### 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
100%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"cost": {
"input_tokens": 379,
"estimated_usd": 0.050907,
"output_tokens": 3318
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 38399,
"response_text": "I'll create the dashboard stats RPC API file that aggregates voter and team data according to the failing tests and acceptance criteria.\n\n```typescript\n// EN: Dashboard statistics API endpoints for voter and team data aggregation\n// VI: API endpoint thống kê dashboard cho tổng hợp dữ liệu cử tri và nhóm\n// ES: Endpoints de API de estadísticas del dashboard para agregación de datos de votantes y equipos\n// SPEC: CP-API-03 - Dashboard stats RPC (aggregate voter/team data)\n\nimport { NextRequest, NextResponse } from 'next/server';\nimport { createClient } from '@supabase/supabase-js';\nimport { z } from 'zod';\n\n// EN: Dashboard stats request schema\n// VI: Schema yêu cầu thống kê dashboard\n// ES: Schema de solicitud de estadísticas del dashboard\nconst DashboardStatsRequestSchema = z.object({\n timeframe: z.enum(['7d', '30d', '90d', '1y']).optional().default('30d'),\n includeDetails: z.boolean().optional().default(false)\n});\n\n// EN: Recent activity request schema\n// VI: Schema yêu cầu hoạt động gần đây\n// ES: Schema de solicitud de actividad reciente\nconst RecentActivityRequestSchema = z.object({\n limit: z.number().min(1).max(100).optional().default(10),\n offset: z.number().min(0).optional().default(0)\n});\n\n// EN: Dashboard stats response type\n// VI: Kiểu phản hồi thống kê dashboard\n// ES: Tipo de respuesta de estadísticas del dashboard\ninterface DashboardStats {\n voter_stats: {\n total_voters: number;\n active_voters: number;\n sentiment_count: {\n positive: number;\n neutral: number;\n negative: number;\n };\n engagement_rate: number;\n };\n team_stats: {\n total_teams: number;\n active_teams: number;\n total_members: number;\n average_team_size: number;\n };\n campaign_metrics: {\n total_campaigns: number;\n active_campaigns: number;\n completion_rate: number;\n };\n timeframe: string;\n}\n\n// EN: Recent activity response type\n// VI: Kiểu phản hồi hoạt động gần đây\n// ES: Tipo de respuesta de actividad reciente\ninterface RecentActivity {\n id: string;\n type: 'voter_registration' | 'team_created' | 'campaign_started' | 'sentiment_recorded';\n description: string;\n timestamp: string;\n user_id?: string;\n metadata?: Record<string, any>;\n}\n\n// EN: Get dashboard statistics with voter and team aggregations\n// VI: Lấy thống kê dashboard với tổng hợp cử tri và nhóm\n// ES: Obtener estadísticas del dashboard con agregaciones de votantes y equipos\nexport async function getDashboardStats(request: NextRequest): Promise<NextResponse> {\n try {\n const url = new URL(request.url);\n const timeframe = url.searchParams.get('timeframe') || '30d';\n const includeDetails = url.searchParams.get('includeDetails') === 'true';\n\n // EN: Validate request parameters\n // VI: Xác thực tham số yêu cầu\n // ES: Validar parámetros de solicitud\n const validation = DashboardStatsRequestSchema.safeParse({\n timeframe,\n includeDetails\n });\n\n if (!validation.success) {\n return NextResponse.json(\n { error: 'Invalid request parameters' },\n { status: 400 }\n );\n }\n\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;\n\n if (!supabaseUrl || !supabaseKey) {\n return NextResponse.json(\n { error: 'Supabase configuration missing' },\n { status: 500 }\n );\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const supabase = createClient(supabaseUrl, supabaseKey) as any;\n\n // EN: Calculate date range based on timeframe\n // VI: Tính toán phạm vi ngày dựa trên khung thời gian\n // ES: Calcular rango de fechas basado en el marco temporal\n const now = new Date();\n const timeframeDays = {\n '7d': 7,\n '30d': 30,\n '90d': 90,\n '1y': 365\n }[validation.data.timeframe];\n\n const startDate = new Date(now.getTime() - (timeframeDays * 24 * 60 * 60 * 1000));\n\n // EN: Aggregate voter statistics with sentiment analysis\n // VI: Tổng hợp thống kê cử tri với phân tích cảm xúc\n // ES: Agregar estadísticas de votantes con análisis de sentimientos\n const { data: voterData, error: voterError } = await supabase\n .rpc('get_voter_stats_with_sentiment', {\n start_date: startDate.toISOString(),\n end_date: now.toISOString()\n });\n\n if (voterError) {\n console.error('Voter stats error:', voterError);\n return NextResponse.json(\n { error: 'Failed to fetch voter statistics' },\n { status: 500 }\n );\n }\n\n // EN: Aggregate team statistics\n // VI: Tổng hợp thống kê nhóm\n // ES: Agregar estadísticas de equipos\n const { data: teamData, error: teamError } = await supabase\n .rpc('get_team_stats', {\n start_date: startDate.toISOString(),\n end_date: now.toISOString()\n });\n\n if (teamError) {\n console.error('Team stats error:', teamError);\n return NextResponse.json(\n { error: 'Failed to fetch team statistics' },\n { status: 500 }\n );\n }\n\n // EN: Aggregate campaign metrics\n // VI: Tổng hợp số liệu chiến dịch\n // ES: Agregar métricas de campañas\n const { data: campaignData, error: campaignError } = await supabase\n .rpc('get_campaign_metrics', {\n start_date: startDate.toISOString(),\n end_date: now.toISOString()\n });\n\n if (campaignError) {\n console.error('Campaign stats error:', campaignError);\n return NextResponse.json(\n { error: 'Failed to fetch campaign metrics' },\n { status: 500 }\n );\n }\n\n // EN: Compile dashboard statistics\n // VI: Biên dịch thống kê dashboard\n // ES: Compilar estadísticas del dashboard\n const stats: DashboardStats = {\n voter_stats: {\n total_voters: voterData?.total_voters || 0,\n active_voters: voterData?.active_voters || 0,\n sentiment_count: {\n positive: voterData?.sentiment_count?.positive || 0,\n neutral: voterData?.sentiment_count?.neutral || 0,\n negative: voterData?.sentiment_count?.negative || 0\n },\n engagement_rate: voterData?.engagement_rate || 0\n },\n team_stats: {\n total_teams: teamData?.total_teams || 0,\n active_teams: teamData?.active_teams || 0,\n total_members: teamData?.total_members || 0,\n average_team_size: teamData?.average_team_size || 0\n },\n campaign_metrics: {\n total_campaigns: campaignData?.total_campaigns || 0,\n active_campaigns: campaignData?.active_campaigns || 0,\n completion_rate: campaignData?.completion_rate || 0\n },\n timeframe: validation.data.timeframe\n };\n\n return NextResponse.json(stats);\n\n } catch (error) {\n console.error('Dashboard stats error:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Get recent activity feed for dashboard\n// VI: Lấy nguồn cấp hoạt động gần đây cho dashboard\n// ES: Obtener feed de actividad reciente para el dashboard\nexport async function getRecentActivity(request: NextRequest): Promise<NextResponse> {\n try {\n const url = new URL(request.url);\n const limit = parseInt(url.searchParams.get('limit') || '10');\n const offset = parseInt(url.searchParams.get('offset') || '0');\n\n // EN: Validate request parameters\n // VI: Xác thực tham số yêu cầu\n // ES: Validar parámetros de solicitud\n const validation = RecentActivityRequestSchema.safeParse({\n limit,\n offset\n });\n\n if (!validation.success) {\n return NextResponse.json(\n { error: 'Invalid request parameters' },\n { status: 400 }\n );\n }\n\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;\n\n if (!supabaseUrl || !supabaseKey) {\n return NextResponse.json(\n { error: 'Supabase configuration missing' },\n { status: 500 }\n );\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const supabase = createClient(supabaseUrl, supabaseKey) as any;\n\n // EN: Fetch recent activity from various sources\n // VI: Lấy hoạt động gần đây từ các nguồn khác nhau\n // ES: Obtener actividad reciente de varias fuentes\n const { data: recent_activity, error } = await supabase\n .rpc('get_recent_activity_feed', {\n limit_count: validation.data.limit,\n offset_count: validation.data.offset\n });\n\n if (error) {\n console.error('Recent activity error:', error);\n return NextResponse.json(\n { error: 'Failed to fetch recent activity' },\n { status: 500 }\n );\n }\n\n return NextResponse.json({\n recent_activity: recent_activity || [],\n pagination: {\n limit: validation.data.limit,\n offset: validation.data.offset,\n has_more: (recent_activity?.length || 0) === validation.data.limit\n }\n });\n\n } catch (error) {\n console.error('Recent activity error:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Main API handler for dashboard stats endpoints\n// VI: Xử lý API chính cho các endpoint thống kê dashboard\n// ES: Manejador de API principal para endpoints de estadísticas del dashboard\nexport async function GET(request: NextRequest): Promise<NextResponse> {\n const url = new URL(request.url);\n const endpoint = url.searchParams.get('endpoint');\n\n switch (endpoint) {\n case 'stats':\n return getDashboardStats(request);\n case 'activity':\n return getRecentActivity(request);\n default:\n return NextResponse.json(\n { error: 'Invalid endpoint' },\n { status: 400 }\n );\n }\n}\n\nexport async function POST(request: NextRequest): Promise<NextResponse> {\n return NextResponse.json(\n { error: 'Method not allowed' },\n { status: 405 }\n );\n}\n```\n\nThis implementation:\n\n1. **Creates the required file** `campaignpro/src/api/stats.ts`\n\n2. **Includes voter stats with sentiment count** - The `getDashboardStats` function aggregates voter statistics including sentiment analysis with positive, neutral, and negative counts\n\n3. **Includes recent activity functionality** - The `getRecentActivity` function fetches and returns recent activity data for the dashboard\n\n4. **Follows codebase conventions**:\n - Trilingual comments (EN/VI/ES)\n - SPEC section reference in header\n - Zod schemas with `.safeParse()`\n - `(supabase as any)` with eslint-disable comment\n - `NextResponse.json()` for responses\n - Consistent error format `{ error: 'message' }`\n\n5. **Provides comprehensive dashboard statistics**:\n - Voter statistics with sentiment analysis\n - Team statistics and metrics\n - Campaign performance metrics\n - Recent activity feed with pagination\n\nThe file contains the required patterns for both sentiment/voter stats and recent activity functionality as specified in the failing tests."
}Task ID: 9478937a-1b10-41b5-8399-98e5b5e6e99f