FOUNDRY
C8 Platform
← 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: FAIL
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
1d ago
Completed
1d ago

Quality

Overall
79%
Pass
PASS
Graded By
-
Graded At
-

Cost

Input Tokens
-
Output Tokens
-
Est. USD
-

Result

{
  "cost": {
    "input_tokens": 339,
    "estimated_usd": 0.100242,
    "output_tokens": 6615
  },
  "model": "claude-sonnet-4-20250514",
  "status": "success",
  "transport": "anthropic_api",
  "latency_ms": 66899,
  "response_text": "I'll create the VoterDetailScreen component with edit functionality and full voter history display.\n\n```typescript\n// campaignpro/src/screens/VoterDetailScreen.tsx\n// EN: Individual voter detail screen with edit capabilities and full history\n// VI: Màn hình chi tiết cử tri với khả năng chỉnh sửa và lịch sử đầy đủ\n// ES: Pantalla de detalle del votante con capacidades de edición e historial completo\n\n'use client';\n\nimport React, { useState, useEffect } from 'react';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport { Label } from '@/components/ui/label';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';\nimport { Textarea } from '@/components/ui/textarea';\nimport { Badge } from '@/components/ui/badge';\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';\nimport { Separator } from '@/components/ui/separator';\nimport { Avatar, AvatarFallback, AvatarInitials } from '@/components/ui/avatar';\nimport { Edit2, Save, X, Phone, Mail, MapPin, Calendar, User, FileText, Clock } from 'lucide-react';\nimport { toast } from '@/components/ui/use-toast';\nimport { z } from 'zod';\n\n// EN: SPEC [CP-API-01] - Voter data validation schema\n// VI: Lược đồ xác thực dữ liệu cử tri\n// ES: Esquema de validación de datos del votante\nconst VoterSchema = z.object({\n  id: z.string(),\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  city: z.string().optional(),\n  state: z.string().optional(),\n  zipCode: z.string().optional(),\n  voterStatus: z.enum(['registered', 'unregistered', 'inactive']),\n  partyAffiliation: z.enum(['democrat', 'republican', 'independent', 'other', 'unknown']),\n  likelihood: z.enum(['strong_support', 'lean_support', 'undecided', 'lean_oppose', 'strong_oppose']),\n  notes: z.string().optional(),\n  tags: z.array(z.string()).optional(),\n});\n\n// EN: Contact history item schema\n// VI: Lược đồ mục lịch sử liên hệ\n// ES: Esquema de elemento del historial de contacto\nconst ContactHistorySchema = z.object({\n  id: z.string(),\n  date: z.string(),\n  type: z.enum(['call', 'email', 'door_knock', 'event', 'mail']),\n  outcome: z.enum(['contacted', 'no_answer', 'busy', 'refused', 'callback']),\n  notes: z.string().optional(),\n  volunteer: z.string(),\n});\n\ntype Voter = z.infer<typeof VoterSchema>;\ntype ContactHistory = z.infer<typeof ContactHistorySchema>;\n\ninterface VoterDetailScreenProps {\n  voterId: string;\n  onClose?: () => void;\n  onUpdate?: (voter: Voter) => void;\n}\n\nexport default function VoterDetailScreen({ voterId, onClose, onUpdate }: VoterDetailScreenProps) {\n  // EN: SPEC [CP-SCREEN-01] - Edit mode state management\n  // VI: Quản lý trạng thái chế độ chỉnh sửa\n  // ES: Gestión del estado del modo de edición\n  const [isEditing, setIsEditing] = useState(false);\n  const [editingData, setEditingData] = useState<Voter | null>(null);\n  const [originalData, setOriginalData] = useState<Voter | null>(null);\n  const [contactHistory, setContactHistory] = useState<ContactHistory[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [saving, setSaving] = useState(false);\n\n  // EN: Load voter data and history on mount\n  // VI: Tải dữ liệu cử tri và lịch sử khi mount\n  // ES: Cargar datos del votante e historial al montar\n  useEffect(() => {\n    loadVoterData();\n    loadContactHistory();\n  }, [voterId]);\n\n  const loadVoterData = async () => {\n    try {\n      setLoading(true);\n      // EN: SPEC [CP-API-01] - Fetch voter details\n      // VI: Lấy chi tiết cử tri\n      // ES: Obtener detalles del votante\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      setOriginalData(result.data);\n      setEditingData(result.data);\n    } catch (error) {\n      console.error('Error loading voter:', error);\n      toast({\n        title: \"Error\",\n        description: \"Failed to load voter details\",\n        variant: \"destructive\",\n      });\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const loadContactHistory = async () => {\n    try {\n      // EN: SPEC [CP-API-01] - Fetch contact history\n      // VI: Lấy lịch sử liên hệ\n      // ES: Obtener historial de contacto\n      const response = await fetch(`/api/voters/${voterId}/history`);\n      const data = await response.json();\n      \n      if (response.ok) {\n        setContactHistory(data);\n      }\n    } catch (error) {\n      console.error('Error loading contact history:', error);\n    }\n  };\n\n  const handleEdit = () => {\n    setIsEditing(true);\n    setEditingData(originalData);\n  };\n\n  const handleCancelEdit = () => {\n    setIsEditing(false);\n    setEditingData(originalData);\n  };\n\n  const handleSave = async () => {\n    if (!editingData) return;\n\n    // EN: Validate edited data\n    // VI: Xác thực dữ liệu đã chỉnh sửa\n    // ES: Validar datos editados\n    const result = VoterSchema.safeParse(editingData);\n    if (!result.success) {\n      toast({\n        title: \"Validation Error\",\n        description: result.error.errors[0].message,\n        variant: \"destructive\",\n      });\n      return;\n    }\n\n    try {\n      setSaving(true);\n      // EN: SPEC [CP-API-01] - Update voter data\n      // VI: Cập nhật dữ liệu cử tri\n      // ES: Actualizar datos del votante\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 responseData = await response.json();\n      \n      if (!response.ok) {\n        throw new Error(responseData.error || 'Failed to update voter');\n      }\n\n      setOriginalData(result.data);\n      setIsEditing(false);\n      onUpdate?.(result.data);\n      \n      toast({\n        title: \"Success\",\n        description: \"Voter updated successfully\",\n      });\n    } catch (error) {\n      console.error('Error saving voter:', error);\n      toast({\n        title: \"Error\",\n        description: \"Failed to save changes\",\n        variant: \"destructive\",\n      });\n    } finally {\n      setSaving(false);\n    }\n  };\n\n  const handleInputChange = (field: keyof Voter, value: any) => {\n    if (!editingData) return;\n    setEditingData({ ...editingData, [field]: value });\n  };\n\n  const formatContactType = (type: string) => {\n    const types = {\n      call: 'Phone Call',\n      email: 'Email',\n      door_knock: 'Door Knock',\n      event: 'Event',\n      mail: 'Mail'\n    };\n    return types[type as keyof typeof types] || type;\n  };\n\n  const formatOutcome = (outcome: string) => {\n    const outcomes = {\n      contacted: 'Contacted',\n      no_answer: 'No Answer',\n      busy: 'Busy',\n      refused: 'Refused',\n      callback: 'Callback Requested'\n    };\n    return outcomes[outcome as keyof typeof outcomes] || outcome;\n  };\n\n  const getLikelihoodColor = (likelihood: string) => {\n    const colors = {\n      strong_support: 'bg-green-100 text-green-800',\n      lean_support: 'bg-green-50 text-green-700',\n      undecided: 'bg-yellow-100 text-yellow-800',\n      lean_oppose: 'bg-red-50 text-red-700',\n      strong_oppose: 'bg-red-100 text-red-800'\n    };\n    return colors[likelihood as keyof typeof colors] || 'bg-gray-100 text-gray-800';\n  };\n\n  if (loading || !originalData) {\n    return (\n      <div className=\"flex items-center justify-center h-96\">\n        <div className=\"animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600\"></div>\n      </div>\n    );\n  }\n\n  const displayData = isEditing ? editingData : originalData;\n  if (!displayData) return null;\n\n  return (\n    <div className=\"max-w-4xl mx-auto p-6 space-y-6\">\n      {/* EN: Header with voter basic info and actions */}\n      {/* VI: Header với thông tin cơ bản và hành động */}\n      {/* ES: Encabezado con información básica y acciones */}\n      <Card>\n        <CardHeader>\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex items-center space-x-4\">\n              <Avatar className=\"h-16 w-16\">\n                <AvatarFallback>\n                  <AvatarInitials name={`${displayData.firstName} ${displayData.lastName}`} />\n                </AvatarFallback>\n              </Avatar>\n              <div>\n                <CardTitle className=\"text-2xl\">\n                  {displayData.firstName} {displayData.lastName}\n                </CardTitle>\n                <div className=\"flex items-center space-x-2 mt-2\">\n                  <Badge className={getLikelihoodColor(displayData.likelihood)}>\n                    {displayData.likelihood.replace('_', ' ')}\n                  </Badge>\n                  <Badge variant=\"outline\">\n                    {displayData.voterStatus}\n                  </Badge>\n                  <Badge variant=\"outline\">\n                    {displayData.partyAffiliation}\n                  </Badge>\n                </div>\n              </div>\n            </div>\n            <div className=\"flex items-center space-x-2\">\n              {!isEditing ? (\n                <>\n                  <Button onClick={handleEdit} size=\"sm\">\n                    <Edit2 className=\"h-4 w-4 mr-2\" />\n                    Edit\n                  </Button>\n                  {onClose && (\n                    <Button onClick={onClose} variant=\"outline\" size=\"sm\">\n                      <X className=\"h-4 w-4 mr-2\" />\n                      Close\n                    </Button>\n                  )}\n                </>\n              ) : (\n                <>\n                  <Button onClick={handleSave} disabled={saving} size=\"sm\">\n                    <Save className=\"h-4 w-4 mr-2\" />\n                    {saving ? 'Saving...' : 'Save'}\n                  </Button>\n                  <Button onClick={handleCancelEdit} variant=\"outline\" size=\"sm\">\n                    <X className=\"h-4 w-4 mr-2\" />\n                    Cancel\n                  </Button>\n                </>\n              )}\n            </div>\n          </div>\n        </CardHeader>\n      </Card>\n\n      {/* EN: Tabbed content with voter details and history */}\n      {/* VI: Nội dung tab với chi tiết cử tri và lịch sử */}\n      {/* ES: Contenido con pestañas con detalles del votante e historial */}\n      <Tabs defaultValue=\"details\" className=\"space-y-4\">\n        <TabsList>\n          <TabsTrigger value=\"details\">\n            <User className=\"h-4 w-4 mr-2\" />\n            Details\n          </TabsTrigger>\n          <TabsTrigger value=\"history\">\n            <Clock className=\"h-4 w-4 mr-2\" />\n            Contact History ({contactHistory.length})\n          </TabsTrigger>\n        </TabsList>\n\n        <TabsContent value=\"details\">\n          <Card>\n            <CardContent className=\"p-6\">\n              <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n                {/* EN: Personal Information */}\n                {/* VI: Thông tin cá nhân */}\n                {/* ES: Información personal */}\n                <div className=\"space-y-4\">\n                  <h3 className=\"font-semibold text-lg flex items-center\">\n                    <User className=\"h-5 w-5 mr-2\" />\n                    Personal Information\n                  </h3>\n                  \n                  <div className=\"space-y-4\">\n                    <div className=\"grid grid-cols-2 gap-4\">\n                      <div>\n                        <Label htmlFor=\"firstName\">First Name</Label>\n                        {isEditing ? (\n                          <Input\n                            id=\"firstName\"\n                            value={displayData.firstName}\n                            onChange={(e) => handleInputChange('firstName', e.target.value)}\n                          />\n                        ) : (\n                          <p className=\"mt-1 text-sm\">{displayData.firstName}</p>\n                        )}\n                      </div>\n                      <div>\n                        <Label htmlFor=\"lastName\">Last Name</Label>\n                        {isEditing ? (\n                          <Input\n                            id=\"lastName\"\n                            value={displayData.lastName}\n                            onChange={(e) => handleInputChange('lastName', e.target.value)}\n                          />\n                        ) : (\n                          <p className=\"mt-1 text-sm\">{displayData.lastName}</p>\n                        )}\n                      </div>\n                    </div>\n\n                    <div>\n                      <Label htmlFor=\"email\">Email</Label>\n                      {isEditing ? (\n                        <Input\n                          id=\"email\"\n                          type=\"email\"\n                          value={displayData.email || ''}\n                          onChange={(e) => handleInputChange('email', e.target.value)}\n                        />\n                      ) : (\n                        <p className=\"mt-1 text-sm flex items-center\">\n                          {displayData.email ? (\n                            <>\n                              <Mail className=\"h-4 w-4 mr-2\" />\n                              {displayData.email}\n                            </>\n                          ) : (\n                            'Not provided'\n                          )}\n                        </p>\n                      )}\n                    </div>\n\n                    <div>\n                      <Label htmlFor=\"phone\">Phone</Label>\n                      {isEditing ? (\n                        <Input\n                          id=\"phone\"\n                          value={displayData.phone || ''}\n                          onChange={(e) => handleInputChange('phone', e.target.value)}\n                        />\n                      ) : (\n                        <p className=\"mt-1 text-sm flex items-center\">\n                          {displayData.phone ? (\n                            <>\n                              <Phone className=\"h-4 w-4 mr-2\" />\n                              {displayData.phone}\n                            </>\n                          ) : (\n                            'Not provided'\n                          )}\n                        </p>\n                      )}\n                    </div>\n                  </div>\n                </div>\n\n                {/* EN: Address Information */}\n                {/* VI: Thông tin địa chỉ */}\n                {/* ES: Información de dirección */}\n                <div className=\"space-y-4\">\n                  <h3 className=\"font-semibold text-lg flex items-center\">\n                    <MapPin className=\"h-5 w-5 mr-2\" />\n                    Address\n                  </h3>\n                  \n                  <div className=\"space-y-4\">\n                    <div>\n                      <Label htmlFor=\"address\">Street Address</Label>\n                      {isEditing ? (\n                        <Input\n                          id=\"address\"\n                          value={displayData.address || ''}\n                          onChange={(e) => handleInputChange('address', e.target.value)}\n                        />\n                      ) : (\n                        <p className=\"mt-1 text-sm\">{displayData.address || 'Not provided'}</p>\n                      )}\n                    </div>\n\n                    <div className=\"grid grid-cols-2 gap-4\">\n                      <div>\n                        <Label htmlFor=\"city\">City</Label>\n                        {isEditing ? (\n                          <Input\n                            id=\"city\"\n                            value={displayData.city || ''}\n                            onChange={(e) => handleInputChange('city', e.target.value)}\n                          />\n                        ) : (\n                          <p className=\"mt-1 text-sm\">{displayData.city || 'Not provided'}</p>\n                        )}\n                      </div>\n                      <div>\n                        <Label htmlFor=\"state\">State</Label>\n                        {isEditing ? (\n                          <Input\n                            id=\"state\"\n                            value={displayData.state || ''}\n                            onChange={(e) => handleInputChange('state', e.target.value)}\n                          />\n                        ) : (\n                          <p className=\"mt-1 text-sm\">{displayData.state || 'Not provided'}</p>\n                        )}\n                      </div>\n                    </div>\n\n                    <div>\n                      <Label htmlFor=\"zipCode\">ZIP Code</Label>\n                      {isEditing ? (\n                        <Input\n                          id=\"zipCode\"\n                          value={displayData.zipCode || ''}\n                          onChange={(e) => handleInputChange('zipCode', e.target.value)}\n                        />\n                      ) : (\n                        <p className=\"mt-1 text-sm\">{displayData.zipCode || 'Not provided'}</p>\n                      )}\n                    </div>\n                  </div>\n                </div>\n\n                {/* EN: Voter Status */}\n                {/* VI: Trạng thái cử tri */}\n                {/* ES: Estado del votante */}\n                <div className=\"space-y-4\">\n                  <h3 className=\"font-semibold text-lg\">Voter Status</h3>\n                  \n                  <div className=\"space-y-4\">\n                    <div>\n                      <Label htmlFor=\"voterStatus\">Registration Status</Label>\n                      {isEditing ? (\n                        <Select value={displayData.voterStatus} onValueChange={(value) => handleInputChange('voterStatus', value)}>\n                          <SelectTrigger>\n                            <SelectValue />\n                          </SelectTrigger>\n                          <SelectContent>\n                            <SelectItem value=\"registered\">Registered</SelectItem>\n                            <SelectItem value=\"unregistered\">Unregistered</SelectItem>\n                            <SelectItem value=\"inactive\">Inactive</SelectItem>\n                          </SelectContent>\n                        </Select>\n                      ) : (\n                        <p className=\"mt-1 text-sm capitalize\">{displayData.voterStatus}</p>\n                      )}\n                    </div>\n\n                    <div>\n                      <Label htmlFor=\"partyAffiliation\">Party Affiliation</Label>\n                      {isEditing ? (\n                        <Select value={displayData.partyAffiliation} onValueChange={(value) => handleInputChange('partyAffiliation', value)}>\n                          <SelectTrigger>\n                            <SelectValue />\n                          </SelectTrigger>\n                          <SelectContent>\n                            <SelectItem value=\"democrat\">Democrat</SelectItem>\n                            <SelectItem value=\"republican\">Republican</SelectItem>\n                            <SelectItem value=\"independent\">Independent</SelectItem>\n                            <SelectItem value=\"other\">Other</SelectItem>\n                            <SelectItem value=\"unknown\">Unknown</SelectItem>\n                          </SelectContent>\n                        </Select>\n                      ) : (\n                        <p className=\"mt-1 text-sm capitalize\">{displayData.partyAffiliation}</p>\n                      )}\n                    </div>\n\n                    <div>\n                      <Label htmlFor=\"likelihood\">Support Likelihood</Label>\n                      {isEditing ? (\n                        <Select value={displayData.likelihood} onValueChange={(value) => handleInputChange('likelihood', value)}>\n                          <SelectTrigger>\n                            <SelectValue />\n                          </SelectTrigger>\n                          <SelectContent>\n                            <SelectItem value=\"strong_support\">Strong Support</SelectItem>\n                            <SelectItem value=\"lean_support\">Lean Support</SelectItem>\n                            <SelectItem value=\"undecided\">Undecided</SelectItem>\n                            <SelectItem value=\"lean_oppose\">Lean Oppose</SelectItem>\n                            <SelectItem value=\"strong_oppose\">Strong Oppose</SelectItem>\n                          </SelectContent>\n                        </Select>\n                      ) : (\n                        <p className=\"mt-1 text-sm\">{displayData.likelihood.replace('_', ' ')}</p>\n                      )}\n                    </div>\n                  </div>\n                </div>\n\n                {/* EN: Notes */}\n                {/* VI: Ghi chú */}\n                {/* ES: Notas */}\n                <div className=\"space-y-4\">\n                  <h3 className=\"font-semibold text-lg flex items-center\">\n                    <FileText className=\"h-5 w-5 mr-2\" />\n                    Notes\n                  </h3>\n                  \n                  {isEditing ? (\n                    <Textarea\n                      value={displayData.notes || ''}\n                      onChange={(e) => handleInputChange('notes', e.target.value)}\n                      placeholder=\"Add notes about this voter...\"\n                      rows={4}\n                    />\n                  ) : (\n                    <p className=\"mt-1 text-sm whitespace-pre-wrap\">\n                      {displayData.notes || 'No notes available'}\n                    </p>\n                  )}\n                </div>\n              </div>\n            </CardContent>\n          </Card>\n        </TabsContent>\n\n        <TabsContent value=\"history\">\n          <Card>\n            <CardHeader>\n              <CardTitle>Contact History</CardTitle>\n            </CardHeader>\n            <CardContent>\n              {contactHistory.length === 0 ? (\n                <p className=\"text-center text-gray-500 py-8\">\n                  No contact history available\n                </p>\n              ) : (\n                <div className=\"space-y-4\">\n                  {contactHistory.map((contact, index) => (\n                    <div key={contact.id}>\n                      <div className=\"flex items-start justify-between\">\n                        <div className=\"flex items-start space-x-3\">\n                          <div className=\"flex-shrink-0 w-2 h-2 bg-blue-500 rounded-full mt-2\"></div>\n                          <div>\n                            <div className=\"flex items-center space-x-2\">\n                              <span className=\"font-medium\">{formatContactType(contact.type)}</span>\n                              <Badge variant=\"outline\" size=\"sm\">\n                                {formatOutcome(contact.outcome)}\n                              </Badge>\n                            </div>\n                            <p className=\"text-sm text-gray-500 mt-1\">\n                              <Calendar className=\"h-4 w-4 inline mr-1\" />\n                              {new Date(contact.date).toLocaleString()}\n                            </p>\n                            {contact.notes && (\n                              <p className=\"text-sm mt-2\">{contact.notes}</p>\n                            )}\n                            <p className=\"text-xs text-gray-400 mt-1\">\n                              by {contact.volunteer}\n                            </p>\n                          </div>\n                        </div>\n                      </div>\n                      {index < contactHistory.length - 1 && (\n                        <Separator className=\"my-4\" />\n                      )}\n                    </div>\n                  ))}\n                </div>\n              )}\n            </CardContent>\n          </Card>\n        </TabsContent>\n      </Tabs>\n    </div>\n  );\n}\n```\n\nThis VoterDetailScreen component provides:\n\n1. **Edit Mode**: Full edit functionality with `isEditing` state management\n2. **Comprehensive Display**: Shows all voter information including personal details, address, voter status, and notes\n3. **Contact History**: Complete timeline of interactions with the voter\n4. **Form Validation**: Uses Zod schema validation for data integrity\n5. **Responsive Design**: Works on both desktop and mobile devices\n6. **Visual Feedback**: Loading states, success/error messages, and proper UI feedback\n7. **Tabbed Interface**: Organized sections for details and history\n8. **Badge System**: Visual indicators for voter likelihood, status, and party affiliation\n\nThe component meets all acceptance criteria by including edit mode functionality and providing a complete voter management interface."
}
Task ID: a26be681-dae3-43b1-907b-1b6bc0327664