FOUNDRY
C8 Platform
← 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