← Tasks
[CP-API-01] Voters API module (CRUD for voters table) (retry 1)
completedcode_genP1
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
### Acceptance Criteria
- File `campaignpro/src/api/voters.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' }
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": 210,
"estimated_usd": 0.048945,
"output_tokens": 3221
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 34637,
"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 with CRUD operations for managing voter data\n// VI: Module API cử tri với các thao tác CRUD để quản lý dữ liệu cử tri \n// ES: Módulo API de votantes con operaciones CRUD para gestionar datos de votantes\n\nimport { NextRequest, NextResponse } from 'next/server';\nimport { createClient } from '@supabase/supabase-js';\nimport { z } from 'zod';\n\n// SPEC: CP-API-01 - Voters table CRUD operations\n// EN: Zod schema for voter data validation\n// VI: Schema Zod để xác thực dữ liệu cử tri\n// ES: Schema Zod para validación de datos de votantes\nconst VoterSchema = z.object({\n id: z.string().uuid().optional(),\n first_name: z.string().min(1).max(100),\n last_name: z.string().min(1).max(100),\n email: z.string().email().optional().nullable(),\n phone: z.string().max(20).optional().nullable(),\n address: z.string().max(255).optional().nullable(),\n city: z.string().max(100).optional().nullable(),\n state: z.string().max(50).optional().nullable(),\n zip_code: z.string().max(10).optional().nullable(),\n date_of_birth: z.string().optional().nullable(),\n party_affiliation: z.enum(['Democrat', 'Republican', 'Independent', 'Other']).optional().nullable(),\n voting_history: z.array(z.string()).optional().nullable(),\n tags: z.array(z.string()).optional().nullable(),\n notes: z.string().optional().nullable(),\n created_at: z.string().optional(),\n updated_at: z.string().optional()\n});\n\nconst VoterUpdateSchema = VoterSchema.partial();\nconst VotersListSchema = z.object({\n page: z.number().min(1).default(1),\n limit: z.number().min(1).max(100).default(20),\n search: z.string().optional(),\n party_affiliation: z.enum(['Democrat', 'Republican', 'Independent', 'Other']).optional(),\n city: z.string().optional(),\n state: z.string().optional()\n});\n\n// EN: Initialize Supabase client\n// VI: Khởi tạo client Supabase\n// ES: Inicializar cliente Supabase\nconst supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;\nconst supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst supabase = createClient(supabaseUrl, supabaseKey) as any;\n\n// EN: Create a new voter\n// VI: Tạo cử tri mới\n// ES: Crear un nuevo votante\nexport async function createVoter(voterData: unknown) {\n const validation = VoterSchema.safeParse(voterData);\n \n if (!validation.success) {\n return NextResponse.json(\n { error: 'Invalid voter data provided' },\n { status: 400 }\n );\n }\n\n try {\n const { data, error } = await supabase\n .from('voters')\n .insert([validation.data])\n .select()\n .single();\n\n if (error) {\n return NextResponse.json(\n { error: 'Failed to create voter' },\n { status: 500 }\n );\n }\n\n return NextResponse.json(data, { status: 201 });\n } catch (error) {\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Get voter by ID\n// VI: Lấy thông tin cử tri theo ID\n// ES: Obtener votante por ID\nexport async function getVoterById(id: string) {\n if (!id) {\n return NextResponse.json(\n { error: 'Voter ID is required' },\n { status: 400 }\n );\n }\n\n try {\n const { data, error } = await supabase\n .from('voters')\n .select('*')\n .eq('id', id)\n .single();\n\n if (error || !data) {\n return NextResponse.json(\n { error: 'Voter not found' },\n { status: 404 }\n );\n }\n\n return NextResponse.json(data);\n } catch (error) {\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Get all voters with filtering and pagination\n// VI: Lấy tất cả cử tri với lọc và phân trang\n// ES: Obtener todos los votantes con filtrado y paginación\nexport async function getVoters(queryParams: unknown) {\n const validation = VotersListSchema.safeParse(queryParams);\n \n if (!validation.success) {\n return NextResponse.json(\n { error: 'Invalid query parameters' },\n { status: 400 }\n );\n }\n\n const { page, limit, search, party_affiliation, city, state } = validation.data;\n const offset = (page - 1) * limit;\n\n try {\n let query = supabase\n .from('voters')\n .select('*', { count: 'exact' });\n\n // EN: Apply search filter\n // VI: Áp dụng bộ lọc tìm kiếm\n // ES: Aplicar filtro de búsqueda\n if (search) {\n query = query.or(`first_name.ilike.%${search}%,last_name.ilike.%${search}%,email.ilike.%${search}%`);\n }\n\n // EN: Apply party affiliation filter\n // VI: Áp dụng bộ lọc theo đảng phái\n // ES: Aplicar filtro de afiliación partidaria\n if (party_affiliation) {\n query = query.eq('party_affiliation', party_affiliation);\n }\n\n // EN: Apply city filter\n // VI: Áp dụng bộ lọc theo thành phố\n // ES: Aplicar filtro de ciudad\n if (city) {\n query = query.eq('city', city);\n }\n\n // EN: Apply state filter\n // VI: Áp dụng bộ lọc theo bang\n // ES: Aplicar filtro de estado\n if (state) {\n query = query.eq('state', state);\n }\n\n const { data, error, count } = await query\n .range(offset, offset + limit - 1)\n .order('created_at', { ascending: false });\n\n if (error) {\n return NextResponse.json(\n { error: 'Failed to fetch voters' },\n { status: 500 }\n );\n }\n\n return NextResponse.json({\n data: data || [],\n pagination: {\n page,\n limit,\n total: count || 0,\n totalPages: Math.ceil((count || 0) / limit)\n }\n });\n } catch (error) {\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Update voter by ID\n// VI: Cập nhật thông tin cử tri theo ID\n// ES: Actualizar votante por ID\nexport async function updateVoter(id: string, voterData: unknown) {\n if (!id) {\n return NextResponse.json(\n { error: 'Voter ID is required' },\n { status: 400 }\n );\n }\n\n const validation = VoterUpdateSchema.safeParse(voterData);\n \n if (!validation.success) {\n return NextResponse.json(\n { error: 'Invalid voter data provided' },\n { status: 400 }\n );\n }\n\n try {\n const { data, error } = await supabase\n .from('voters')\n .update({ ...validation.data, updated_at: new Date().toISOString() })\n .eq('id', id)\n .select()\n .single();\n\n if (error || !data) {\n return NextResponse.json(\n { error: 'Failed to update voter or voter not found' },\n { status: 404 }\n );\n }\n\n return NextResponse.json(data);\n } catch (error) {\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Delete voter by ID\n// VI: Xóa cử tri theo ID\n// ES: Eliminar votante por ID\nexport async function deleteVoter(id: string) {\n if (!id) {\n return NextResponse.json(\n { error: 'Voter ID is required' },\n { status: 400 }\n );\n }\n\n try {\n const { error } = await supabase\n .from('voters')\n .delete()\n .eq('id', id);\n\n if (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 } catch (error) {\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Bulk operations for voters\n// VI: Các thao tác hàng loạt cho cử tri\n// ES: Operaciones masivas para votantes\nexport async function bulkCreateVoters(votersData: unknown) {\n if (!Array.isArray(votersData)) {\n return NextResponse.json(\n { error: 'Expected array of voter data' },\n { status: 400 }\n );\n }\n\n const validatedVoters = [];\n const errors = [];\n\n for (let i = 0; i < votersData.length; i++) {\n const validation = VoterSchema.safeParse(votersData[i]);\n if (validation.success) {\n validatedVoters.push(validation.data);\n } else {\n errors.push({ index: i, error: 'Invalid voter data' });\n }\n }\n\n if (errors.length > 0) {\n return NextResponse.json(\n { error: 'Some voter records are invalid', details: errors },\n { status: 400 }\n );\n }\n\n try {\n const { data, error } = await supabase\n .from('voters')\n .insert(validatedVoters)\n .select();\n\n if (error) {\n return NextResponse.json(\n { error: 'Failed to create voters' },\n { status: 500 }\n );\n }\n\n return NextResponse.json({\n message: `Successfully created ${data.length} voters`,\n data\n }, { status: 201 });\n } catch (error) {\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// EN: Export functions for use in Next.js API routes\n// VI: Xuất các hàm để sử dụng trong các tuyến đường API Next.js\n// ES: Exportar funciones para usar en rutas API de Next.js\nexport {\n VoterSchema,\n VoterUpdateSchema,\n VotersListSchema\n};\n```\n\nThis voters API module provides:\n\n1. **CRUD Operations**: Create, read, update, and delete voters\n2. **Data Validation**: Zod schemas for request validation with `.safeParse()`\n3. **Filtering & Pagination**: Search, filter by party/location, with pagination\n4. **Bulk Operations**: Support for creating multiple voters at once\n5. **Error Handling**: Consistent error responses with `{ error: 'message' }` format\n6. **Trilingual Comments**: EN/VI/ES comments throughout\n7. **Supabase Integration**: Uses `(supabase as any)` with eslint-disable\n8. **NextResponse**: All responses use `NextResponse.json()`\n\nThe module follows all specified conventions and provides comprehensive voter management functionality for the campaign platform."
}Task ID: cf8ae4a4-dd62-477e-9aeb-7cdca7251a7b