← Tasks
[CP-SCREEN-01] VoterDetailScreen — view/edit individual voter with full history (retry 1)
completedcode_genP1
Description
## CP-SCREEN-01: VoterDetailScreen — view/edit individual voter with full history
Section: New Screen | Milestone: M3 | Owner: Claude Sonnet
### Failing Tests
- **detail_screen_file** (file_exists): File `campaignpro/src/screens/VoterDetailScreen.tsx` must exist
- **detail_edit_mode** (grep): Pattern `editing|isEditing|setEditing` in `campaignpro/src/screens/VoterDetailScreen.tsx` (need 1+ matches)
### Acceptance Criteria
- File `campaignpro/src/screens/VoterDetailScreen.tsx` must exist
- File matching `campaignpro/src/screens/VoterDetailScreen.tsx` must contain pattern `editing|isEditing|setEditing`
### 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
- CP-WIRE-02: PASS
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
1d ago
Completed
1d ago
Quality
Overall
92%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"cost": {
"input_tokens": 339,
"estimated_usd": 0.064242,
"output_tokens": 4215
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 48128,
"response_text": "I'll create the VoterDetailScreen component that allows viewing and editing individual voter details with full history.\n\n```tsx\n// campaignpro/src/screens/VoterDetailScreen.tsx\n// EN: Screen for viewing and editing individual voter details with complete interaction history\n// VI: Màn hình xem và chỉnh sửa chi tiết cử tri cá nhân với lịch sử tương tác đầy đủ \n// ES: Pantalla para ver y editar detalles individuales del votante con historial completo\n\n'use client';\n\nimport React, { useState, useEffect } from 'react';\nimport { useRouter } from 'next/navigation';\nimport { z } from 'zod';\n\n// EN: Validation schema for voter data\n// VI: Schema xác thực cho dữ liệu cử tri\n// ES: Schema de validación para datos del votante\nconst VoterSchema = z.object({\n id: z.number(),\n firstName: z.string().min(1, 'First name required'),\n lastName: z.string().min(1, 'Last name required'),\n email: z.string().email().optional().or(z.literal('')),\n phone: z.string().optional(),\n address: z.string().optional(),\n votingHistory: z.array(z.object({\n year: z.number(),\n election: z.string(),\n participated: z.boolean()\n })).default([]),\n interactions: z.array(z.object({\n id: z.number(),\n date: z.string(),\n type: z.string(),\n notes: z.string(),\n volunteer: z.string()\n })).default([]),\n tags: z.array(z.string()).default([]),\n supportLevel: z.enum(['strong_support', 'lean_support', 'undecided', 'lean_oppose', 'strong_oppose']).optional()\n});\n\ntype Voter = z.infer<typeof VoterSchema>;\n\ninterface VoterDetailScreenProps {\n voterId: string;\n}\n\n// SPEC: CP-SCREEN-01 - VoterDetailScreen with edit mode functionality\nexport default function VoterDetailScreen({ voterId }: VoterDetailScreenProps) {\n const router = useRouter();\n const [voter, setVoter] = useState<Voter | null>(null);\n const [isEditing, setIsEditing] = useState(false);\n const [editForm, setEditForm] = useState<Partial<Voter>>({});\n const [loading, setLoading] = useState(true);\n const [saving, setSaving] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // EN: Load voter data on component mount\n // VI: Tải dữ liệu cử tri khi component được mount\n // ES: Cargar datos del votante al montar el componente\n useEffect(() => {\n loadVoterData();\n }, [voterId]);\n\n const loadVoterData = async () => {\n try {\n setLoading(true);\n const response = await fetch(`/api/voters/${voterId}`);\n const data = await response.json();\n\n if (!response.ok) {\n throw new Error(data.error || 'Failed to load voter');\n }\n\n const result = VoterSchema.safeParse(data);\n if (!result.success) {\n throw new Error('Invalid voter data format');\n }\n\n setVoter(result.data);\n setEditForm(result.data);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Unknown error');\n } finally {\n setLoading(false);\n }\n };\n\n // EN: Handle entering edit mode\n // VI: Xử lý việc vào chế độ chỉnh sửa\n // ES: Manejar la entrada al modo de edición\n const handleStartEditing = () => {\n setIsEditing(true);\n setEditForm(voter || {});\n };\n\n // EN: Handle canceling edit mode\n // VI: Xử lý việc hủy chế độ chỉnh sửa\n // ES: Manejar la cancelación del modo de edición\n const handleCancelEditing = () => {\n setIsEditing(false);\n setEditForm(voter || {});\n setError(null);\n };\n\n // EN: Handle saving voter changes\n // VI: Xử lý việc lưu thay đổi cử tri\n // ES: Manejar el guardado de cambios del votante\n const handleSaveVoter = async () => {\n try {\n setSaving(true);\n setError(null);\n\n const result = VoterSchema.safeParse(editForm);\n if (!result.success) {\n throw new Error('Please check all required fields');\n }\n\n const response = await fetch(`/api/voters/${voterId}`, {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(result.data),\n });\n\n const data = await response.json();\n\n if (!response.ok) {\n throw new Error(data.error || 'Failed to save voter');\n }\n\n setVoter(result.data);\n setIsEditing(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Save failed');\n } finally {\n setSaving(false);\n }\n };\n\n // EN: Handle form field updates\n // VI: Xử lý cập nhật trường form\n // ES: Manejar actualizaciones de campos del formulario\n const handleFieldChange = (field: keyof Voter, value: any) => {\n setEditForm(prev => ({ ...prev, [field]: value }));\n };\n\n if (loading) {\n return (\n <div className=\"flex justify-center items-center min-h-[400px]\">\n <div className=\"text-lg\">\n Loading voter details... {/* EN: Loading message */}\n </div>\n </div>\n );\n }\n\n if (!voter) {\n return (\n <div className=\"text-center py-8\">\n <div className=\"text-red-600 mb-4\">\n {error || 'Voter not found'}\n </div>\n <button \n onClick={() => router.back()}\n className=\"px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600\"\n >\n Go Back {/* EN: Navigation button */}\n </button>\n </div>\n );\n }\n\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n {/* EN: Header with voter name and action buttons */}\n {/* VI: Header với tên cử tri và nút hành động */}\n {/* ES: Encabezado con nombre del votante y botones de acción */}\n <div className=\"flex justify-between items-center mb-6\">\n <h1 className=\"text-3xl font-bold\">\n {voter.firstName} {voter.lastName}\n </h1>\n \n <div className=\"space-x-2\">\n {!isEditing ? (\n <button\n onClick={handleStartEditing}\n className=\"px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700\"\n >\n Edit {/* EN: Edit button */}\n </button>\n ) : (\n <>\n <button\n onClick={handleCancelEditing}\n className=\"px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600\"\n disabled={saving}\n >\n Cancel {/* EN: Cancel button */}\n </button>\n <button\n onClick={handleSaveVoter}\n className=\"px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700\"\n disabled={saving}\n >\n {saving ? 'Saving...' : 'Save'} {/* EN: Save button with loading state */}\n </button>\n </>\n )}\n </div>\n </div>\n\n {error && (\n <div className=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6\">\n {error}\n </div>\n )}\n\n {/* EN: Main content grid */}\n {/* VI: Lưới nội dung chính */}\n {/* ES: Cuadrícula de contenido principal */}\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-6\">\n \n {/* EN: Basic Information Section */}\n {/* VI: Phần thông tin cơ bản */}\n {/* ES: Sección de información básica */}\n <div className=\"bg-white rounded-lg shadow p-6\">\n <h2 className=\"text-xl font-semibold mb-4\">Basic Information</h2>\n \n {isEditing ? (\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium mb-1\">First Name</label>\n <input\n type=\"text\"\n value={editForm.firstName || ''}\n onChange={(e) => handleFieldChange('firstName', e.target.value)}\n className=\"w-full border rounded px-3 py-2\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium mb-1\">Last Name</label>\n <input\n type=\"text\"\n value={editForm.lastName || ''}\n onChange={(e) => handleFieldChange('lastName', e.target.value)}\n className=\"w-full border rounded px-3 py-2\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium mb-1\">Email</label>\n <input\n type=\"email\"\n value={editForm.email || ''}\n onChange={(e) => handleFieldChange('email', e.target.value)}\n className=\"w-full border rounded px-3 py-2\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium mb-1\">Phone</label>\n <input\n type=\"tel\"\n value={editForm.phone || ''}\n onChange={(e) => handleFieldChange('phone', e.target.value)}\n className=\"w-full border rounded px-3 py-2\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium mb-1\">Address</label>\n <textarea\n value={editForm.address || ''}\n onChange={(e) => handleFieldChange('address', e.target.value)}\n rows={3}\n className=\"w-full border rounded px-3 py-2\"\n />\n </div>\n </div>\n ) : (\n <div className=\"space-y-3\">\n <div><strong>Email:</strong> {voter.email || 'Not provided'}</div>\n <div><strong>Phone:</strong> {voter.phone || 'Not provided'}</div>\n <div><strong>Address:</strong> {voter.address || 'Not provided'}</div>\n {voter.supportLevel && (\n <div><strong>Support Level:</strong> \n <span className=\"ml-2 px-2 py-1 bg-blue-100 rounded text-sm\">\n {voter.supportLevel.replace('_', ' ')}\n </span>\n </div>\n )}\n </div>\n )}\n </div>\n\n {/* EN: Voting History Section */}\n {/* VI: Phần lịch sử bỏ phiếu */}\n {/* ES: Sección de historial de votación */}\n <div className=\"bg-white rounded-lg shadow p-6\">\n <h2 className=\"text-xl font-semibold mb-4\">Voting History</h2>\n \n {voter.votingHistory.length > 0 ? (\n <div className=\"space-y-2\">\n {voter.votingHistory.map((vote, index) => (\n <div key={index} className=\"flex justify-between items-center py-2 border-b\">\n <div>\n <div className=\"font-medium\">{vote.election}</div>\n <div className=\"text-sm text-gray-600\">{vote.year}</div>\n </div>\n <span className={`px-2 py-1 rounded text-xs ${\n vote.participated \n ? 'bg-green-100 text-green-800' \n : 'bg-red-100 text-red-800'\n }`}>\n {vote.participated ? 'Voted' : 'Did not vote'}\n </span>\n </div>\n ))}\n </div>\n ) : (\n <div className=\"text-gray-500 italic\">No voting history available</div>\n )}\n </div>\n </div>\n\n {/* EN: Interaction History Section */}\n {/* VI: Phần lịch sử tương tác */}\n {/* ES: Sección de historial de interacciones */}\n <div className=\"bg-white rounded-lg shadow p-6 mt-6\">\n <h2 className=\"text-xl font-semibold mb-4\">Interaction History</h2>\n \n {voter.interactions.length > 0 ? (\n <div className=\"space-y-4\">\n {voter.interactions.map((interaction) => (\n <div key={interaction.id} className=\"border-l-4 border-blue-500 pl-4 py-2\">\n <div className=\"flex justify-between items-start mb-2\">\n <div className=\"font-medium\">{interaction.type}</div>\n <div className=\"text-sm text-gray-600\">\n {new Date(interaction.date).toLocaleDateString()}\n </div>\n </div>\n <div className=\"text-gray-700 mb-1\">{interaction.notes}</div>\n <div className=\"text-xs text-gray-500\">\n by {interaction.volunteer}\n </div>\n </div>\n ))}\n </div>\n ) : (\n <div className=\"text-gray-500 italic\">No interactions recorded</div>\n )}\n </div>\n\n {/* EN: Tags Section */}\n {/* VI: Phần nhãn */}\n {/* ES: Sección de etiquetas */}\n {voter.tags.length > 0 && (\n <div className=\"bg-white rounded-lg shadow p-6 mt-6\">\n <h2 className=\"text-xl font-semibold mb-4\">Tags</h2>\n <div className=\"flex flex-wrap gap-2\">\n {voter.tags.map((tag, index) => (\n <span \n key={index}\n className=\"px-3 py-1 bg-purple-100 text-purple-800 rounded-full text-sm\"\n >\n {tag}\n </span>\n ))}\n </div>\n </div>\n )}\n </div>\n );\n}\n```\n\nI've created the VoterDetailScreen component that fulfills all the acceptance criteria:\n\n## Key Features:\n\n1. **Edit Mode Functionality**: Uses `isEditing` state with `setIsEditing` functions to toggle between view and edit modes\n2. **Complete Voter Profile**: Shows all voter information including basic details, voting history, interactions, and tags\n3. **Form Validation**: Uses Zod schema validation with `.safeParse()` for data integrity\n4. **Full CRUD Operations**: View, edit, and save voter details\n5. **Interaction History**: Displays complete interaction timeline with dates, types, and notes\n6. **Voting History**: Shows participation in past elections\n7. **Error Handling**: Comprehensive error states and user feedback\n\n## Technical Implementation:\n\n- **State Management**: Uses React hooks for edit mode, loading states, and form data\n- **Validation**: Zod schema at module level with proper error handling\n- **API Integration**: RESTful API calls for loading and saving voter data\n- **Responsive Design**: Grid layout that adapts to different screen sizes\n- **Trilingual Comments**: EN/VI/ES comments throughout\n- **SPEC References**: Includes CP-SCREEN-01 specification reference\n\nThe component provides a complete voter detail interface with the required edit functionality and comprehensive voter history display."
}Task ID: a9737557-8674-4fb4-8192-57bdd7300878