← Tasks
[CP-API-01] Voters API module (CRUD for voters table) (retry 1) (retry 2)
completedcode_genP0
Description
## CP-API-01: Voters API module (CRUD for voters table)
Section: API | Milestone: M3 | Owner: AG Lu
### Failing Tests
- **voters_api_file** (file_exists): File `campaignpro/src/api/voters.ts` must exist
- **voters_get** (grep): Pattern `getVoters|listVoters` in `campaignpro/src/api/voters.ts` (need 1+ matches)
- **voters_create** (grep): Pattern `createVoter|insertVoter` in `campaignpro/src/api/voters.ts` (need 1+ matches)
- **voters_update** (grep): Pattern `updateVoter` in `campaignpro/src/api/voters.ts` (need 1+ matches)
### Acceptance Criteria
- File `campaignpro/src/api/voters.ts` must exist
- File matching `campaignpro/src/api/voters.ts` must contain pattern `getVoters|listVoters`
- File matching `campaignpro/src/api/voters.ts` must contain pattern `createVoter|insertVoter`
- File matching `campaignpro/src/api/voters.ts` must contain pattern `updateVoter`
### 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' }
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": 433,
"estimated_usd": 0.051954,
"output_tokens": 3377
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 36200,
"response_text": "I'll create the voters API module with CRUD operations for the voters table.\n\n```typescript\n// campaignpro/src/api/voters.ts\n// EN: Voters API module providing CRUD operations for voter management\n// VI: Module API cử tri cung cấp các thao tác CRUD để quản lý cử tri\n// ES: Módulo API de votantes que proporciona operaciones CRUD para gestión de votantes\n// SPEC: CP-API-01 - Voters API module (CRUD for voters table)\n\nimport { createClient } from '@supabase/supabase-js';\nimport { NextResponse } from 'next/server';\nimport { z } from 'zod';\n\n// EN: Supabase client setup with environment variables\n// VI: Thiết lập client Supabase với các biến môi trường\n// ES: Configuración del cliente Supabase con variables de entorno\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!) as any;\n\n// EN: Voter data validation schema\n// VI: Schema xác thực dữ liệu cử tri\n// ES: Esquema de validación de datos del votante\nconst voterSchema = z.object({\n first_name: z.string().min(1, 'First name is required'),\n last_name: z.string().min(1, 'Last name is required'),\n email: z.string().email('Valid email is required').optional(),\n phone: z.string().optional(),\n address: z.string().optional(),\n city: z.string().optional(),\n state: z.string().optional(),\n zip_code: z.string().optional(),\n date_of_birth: z.string().optional(),\n party_affiliation: z.string().optional(),\n voter_registration_number: z.string().optional(),\n voting_district: z.string().optional(),\n notes: z.string().optional(),\n tags: z.array(z.string()).optional()\n});\n\nconst voterUpdateSchema = voterSchema.partial();\n\n// EN: Get all voters with optional filtering and pagination\n// VI: Lấy tất cả cử tri với tùy chọn lọc và phân trang\n// ES: Obtener todos los votantes con filtrado opcional y paginación\nexport async function getVoters(params?: {\n page?: number;\n limit?: number;\n search?: string;\n party_affiliation?: string;\n voting_district?: string;\n}) {\n try {\n const { page = 1, limit = 50, search, party_affiliation, voting_district } = params || {};\n const offset = (page - 1) * limit;\n\n let query = supabase\n .from('voters')\n .select('*', { count: 'exact' })\n .range(offset, offset + limit - 1)\n .order('created_at', { ascending: false });\n\n // EN: Apply search filter if provided\n // VI: Áp dụng bộ lọc tìm kiếm nếu được cung cấp\n // ES: Aplicar filtro de búsqueda si se proporciona\n if (search) {\n query = query.or(`first_name.ilike.%${search}%,last_name.ilike.%${search}%,email.ilike.%${search}%`);\n }\n\n if (party_affiliation) {\n query = query.eq('party_affiliation', party_affiliation);\n }\n\n if (voting_district) {\n query = query.eq('voting_district', voting_district);\n }\n\n const { data, error, count } = await query;\n\n if (error) {\n console.error('Error fetching voters:', error);\n return NextResponse.json(\n { error: 'Failed to fetch voters' },\n { status: 500 }\n );\n }\n\n return NextResponse.json({\n voters: data || [],\n pagination: {\n page,\n limit,\n total: count || 0,\n totalPages: Math.ceil((count || 0) / limit)\n }\n });\n\n } catch (error) {\n console.error('Unexpected error in getVoters:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Alternative function name for listing voters\n// VI: Tên hàm thay thế để liệt kê cử tri\n// ES: Nombre alternativo de función para listar votantes\nexport const listVoters = getVoters;\n\n// EN: Create a new voter record\n// VI: Tạo bản ghi cử tri mới\n// ES: Crear un nuevo registro de votante\nexport async function createVoter(voterData: unknown) {\n try {\n // EN: Validate voter data using Zod schema\n // VI: Xác thực dữ liệu cử tri bằng schema Zod\n // ES: Validar datos del votante usando esquema Zod\n const validation = voterSchema.safeParse(voterData);\n \n if (!validation.success) {\n return NextResponse.json(\n { error: 'Invalid voter data', details: validation.error.errors },\n { status: 400 }\n );\n }\n\n const { data, error } = await supabase\n .from('voters')\n .insert([validation.data])\n .select()\n .single();\n\n if (error) {\n console.error('Error creating voter:', error);\n return NextResponse.json(\n { error: 'Failed to create voter' },\n { status: 500 }\n );\n }\n\n return NextResponse.json({ voter: data }, { status: 201 });\n\n } catch (error) {\n console.error('Unexpected error in createVoter:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Alternative function name for inserting a voter\n// VI: Tên hàm thay thế để chèn cử tri\n// ES: Nombre alternativo de función para insertar votante\nexport const insertVoter = createVoter;\n\n// EN: Update an existing voter record\n// VI: Cập nhật bản ghi cử tri hiện có\n// ES: Actualizar un registro de votante existente\nexport async function updateVoter(voterId: string, voterData: unknown) {\n try {\n // EN: Validate partial voter data for updates\n // VI: Xác thực dữ liệu cử tri một phần cho cập nhật\n // ES: Validar datos parciales del votante para actualizaciones\n const validation = voterUpdateSchema.safeParse(voterData);\n \n if (!validation.success) {\n return NextResponse.json(\n { error: 'Invalid voter data', details: validation.error.errors },\n { status: 400 }\n );\n }\n\n // EN: Check if voter exists before updating\n // VI: Kiểm tra cử tri có tồn tại trước khi cập nhật\n // ES: Verificar si el votante existe antes de actualizar\n const { data: existingVoter, error: fetchError } = await supabase\n .from('voters')\n .select('id')\n .eq('id', voterId)\n .single();\n\n if (fetchError || !existingVoter) {\n return NextResponse.json(\n { error: 'Voter not found' },\n { status: 404 }\n );\n }\n\n const { data, error } = await supabase\n .from('voters')\n .update({\n ...validation.data,\n updated_at: new Date().toISOString()\n })\n .eq('id', voterId)\n .select()\n .single();\n\n if (error) {\n console.error('Error updating voter:', error);\n return NextResponse.json(\n { error: 'Failed to update voter' },\n { status: 500 }\n );\n }\n\n return NextResponse.json({ voter: data });\n\n } catch (error) {\n console.error('Unexpected error in updateVoter:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Get a single voter by ID\n// VI: Lấy một cử tri theo ID\n// ES: Obtener un votante por ID\nexport async function getVoterById(voterId: string) {\n try {\n const { data, error } = await supabase\n .from('voters')\n .select('*')\n .eq('id', voterId)\n .single();\n\n if (error) {\n if (error.code === 'PGRST116') {\n return NextResponse.json(\n { error: 'Voter not found' },\n { status: 404 }\n );\n }\n console.error('Error fetching voter:', error);\n return NextResponse.json(\n { error: 'Failed to fetch voter' },\n { status: 500 }\n );\n }\n\n return NextResponse.json({ voter: data });\n\n } catch (error) {\n console.error('Unexpected error in getVoterById:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Delete a voter record\n// VI: Xóa bản ghi cử tri\n// ES: Eliminar registro de votante\nexport async function deleteVoter(voterId: string) {\n try {\n const { error } = await supabase\n .from('voters')\n .delete()\n .eq('id', voterId);\n\n if (error) {\n console.error('Error deleting voter:', error);\n return NextResponse.json(\n { error: 'Failed to delete voter' },\n { status: 500 }\n );\n }\n\n return NextResponse.json({ message: 'Voter deleted successfully' });\n\n } catch (error) {\n console.error('Unexpected error in deleteVoter:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Get voter statistics and counts\n// VI: Lấy thống kê và số lượng cử tri\n// ES: Obtener estadísticas y recuentos de votantes\nexport async function getVoterStats() {\n try {\n const { data: totalCount, error: totalError } = await supabase\n .from('voters')\n .select('id', { count: 'exact', head: true });\n\n if (totalError) {\n console.error('Error fetching voter stats:', totalError);\n return NextResponse.json(\n { error: 'Failed to fetch voter statistics' },\n { status: 500 }\n );\n }\n\n // EN: Get party affiliation breakdown\n // VI: Lấy phân tích theo đảng phái\n // ES: Obtener desglose por afiliación partidaria\n const { data: partyStats, error: partyError } = await supabase\n .from('voters')\n .select('party_affiliation')\n .not('party_affiliation', 'is', null);\n\n if (partyError) {\n console.error('Error fetching party stats:', partyError);\n }\n\n const partyBreakdown = partyStats?.reduce((acc: Record<string, number>, voter) => {\n const party = voter.party_affiliation || 'Unknown';\n acc[party] = (acc[party] || 0) + 1;\n return acc;\n }, {}) || {};\n\n return NextResponse.json({\n totalVoters: totalCount || 0,\n partyBreakdown\n });\n\n } catch (error) {\n console.error('Unexpected error in getVoterStats:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n```\n\nThis voters API module provides comprehensive CRUD operations for the voters table, including:\n\n**Key Features:**\n- **getVoters/listVoters**: Retrieve voters with filtering, search, and pagination\n- **createVoter/insertVoter**: Create new voter records with validation\n- **updateVoter**: Update existing voter records\n- **getVoterById**: Retrieve a specific voter by ID\n- **deleteVoter**: Remove voter records\n- **getVoterStats**: Get voter statistics and party breakdown\n\n**Following Conventions:**\n- ✅ Trilingual comments (EN/VI/ES)\n- ✅ SPEC section reference (CP-API-01)\n- ✅ Zod validation with .safeParse()\n- ✅ (supabase as any) with eslint-disable\n- ✅ NextResponse.json() for all responses\n- ✅ Consistent error format: { error: 'message' }\n\nThe module satisfies all acceptance criteria by including the required function patterns and providing a robust API for voter management."
}Task ID: 7c737896-bf3c-4d3d-8568-9f13857649be