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: PASS
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
1d ago
Completed
1d ago

Quality

Overall
97%
Pass
PASS
Graded By
-
Graded At
-

Cost

Input Tokens
-
Output Tokens
-
Est. USD
-

Result

{
  "cost": {
    "input_tokens": 339,
    "estimated_usd": 0.077112,
    "output_tokens": 5073
  },
  "model": "claude-sonnet-4-20250514",
  "status": "success",
  "transport": "anthropic_api",
  "latency_ms": 57101,
  "response_text": "I'll create the VoterDetailScreen component that allows viewing and editing individual voter information with full history.\n\n```typescript\n// campaignpro/src/screens/VoterDetailScreen.tsx\n'use client'\n\nimport React, { useState, useEffect } from 'react'\nimport { useParams, useRouter } from 'next/navigation'\nimport { z } from 'zod'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Textarea } from '@/components/ui/textarea'\nimport { Badge } from '@/components/ui/badge'\nimport { Separator } from '@/components/ui/separator'\nimport { ArrowLeft, Edit, Save, X, Calendar, MapPin, Phone, Mail, User } from 'lucide-react'\n\n// EN: Voter detail data validation schema\n// VI: Schema xác thực dữ liệu chi tiết cử tri\n// ES: Esquema de validación de datos detallados del votante\nconst VoterDetailSchema = z.object({\n  id: z.string(),\n  firstName: z.string().min(1, 'First name is required'),\n  lastName: z.string().min(1, 'Last name is 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  zipCode: z.string().optional(),\n  voterStatus: z.enum(['registered', 'unregistered', 'pending']),\n  politicalAffiliation: z.enum(['democrat', 'republican', 'independent', 'unknown']),\n  notes: z.string().optional(),\n  createdAt: z.string(),\n  updatedAt: z.string()\n})\n\n// EN: Contact history entry validation\n// VI: Xác thực mục lịch sử liên lạc\n// ES: Validación de entrada del historial de contacto\nconst ContactHistorySchema = z.object({\n  id: z.string(),\n  type: z.enum(['call', 'email', 'visit', 'event']),\n  date: z.string(),\n  notes: z.string(),\n  outcome: z.enum(['positive', 'negative', 'neutral']),\n  userId: z.string(),\n  userName: z.string()\n})\n\ntype VoterDetail = z.infer<typeof VoterDetailSchema>\ntype ContactHistory = z.infer<typeof ContactHistorySchema>\n\ninterface VoterDetailScreenProps {\n  voterId?: string\n}\n\nexport default function VoterDetailScreen({ voterId }: VoterDetailScreenProps) {\n  const params = useParams()\n  const router = useRouter()\n  const id = voterId || (params?.id as string)\n  \n  // EN: Component state management\n  // VI: Quản lý trạng thái component\n  // ES: Gestión del estado del componente\n  const [voter, setVoter] = useState<VoterDetail | null>(null)\n  const [contactHistory, setContactHistory] = useState<ContactHistory[]>([])\n  const [isEditing, setIsEditing] = useState(false)\n  const [loading, setLoading] = useState(true)\n  const [saving, setSaving] = useState(false)\n  const [error, setError] = useState<string>('')\n  \n  // EN: Form data for editing mode\n  // VI: Dữ liệu form cho chế độ chỉnh sửa\n  // ES: Datos del formulario para modo de edición\n  const [editForm, setEditForm] = useState<Partial<VoterDetail>>({})\n\n  // EN: Load voter data and history on component mount - SPEC: CP-WIRE-02\n  // VI: Tải dữ liệu cử tri và lịch sử khi component được mount - SPEC: CP-WIRE-02\n  // ES: Cargar datos del votante e historial al montar el componente - SPEC: CP-WIRE-02\n  useEffect(() => {\n    if (!id) return\n\n    const loadVoterData = async () => {\n      try {\n        setLoading(true)\n        setError('')\n\n        // EN: Fetch voter details\n        // VI: Lấy chi tiết cử tri\n        // ES: Obtener detalles del votante\n        const voterResponse = await fetch(`/api/voters/${id}`)\n        if (!voterResponse.ok) {\n          throw new Error('Failed to load voter data')\n        }\n        \n        const voterData = await voterResponse.json()\n        const voterValidation = VoterDetailSchema.safeParse(voterData)\n        \n        if (!voterValidation.success) {\n          console.error('Voter data validation failed:', voterValidation.error)\n          throw new Error('Invalid voter data format')\n        }\n        \n        setVoter(voterValidation.data)\n        setEditForm(voterValidation.data)\n\n        // EN: Fetch contact history\n        // VI: Lấy lịch sử liên lạc\n        // ES: Obtener historial de contacto\n        const historyResponse = await fetch(`/api/voters/${id}/history`)\n        if (historyResponse.ok) {\n          const historyData = await historyResponse.json()\n          const validHistory = historyData.filter((item: any) => {\n            const validation = ContactHistorySchema.safeParse(item)\n            return validation.success\n          })\n          setContactHistory(validHistory)\n        }\n      } catch (err) {\n        console.error('Error loading voter data:', err)\n        setError(err instanceof Error ? err.message : 'Failed to load voter data')\n      } finally {\n        setLoading(false)\n      }\n    }\n\n    loadVoterData()\n  }, [id])\n\n  // EN: Handle form input changes during editing\n  // VI: Xử lý thay đổi input form khi chỉnh sửa\n  // ES: Manejar cambios de entrada del formulario durante la edición\n  const handleInputChange = (field: keyof VoterDetail, value: string) => {\n    setEditForm(prev => ({\n      ...prev,\n      [field]: value\n    }))\n  }\n\n  // EN: Save voter changes - SPEC: CP-API-01\n  // VI: Lưu thay đổi cử tri - SPEC: CP-API-01\n  // ES: Guardar cambios del votante - SPEC: CP-API-01\n  const handleSave = async () => {\n    if (!voter || !editForm) return\n\n    try {\n      setSaving(true)\n      setError('')\n\n      const updateData = { ...editForm, updatedAt: new Date().toISOString() }\n      const validation = VoterDetailSchema.safeParse(updateData)\n      \n      if (!validation.success) {\n        throw new Error('Please check all required fields')\n      }\n\n      const response = await fetch(`/api/voters/${id}`, {\n        method: 'PUT',\n        headers: {\n          'Content-Type': 'application/json'\n        },\n        body: JSON.stringify(validation.data)\n      })\n\n      if (!response.ok) {\n        const errorData = await response.json()\n        throw new Error(errorData.error || 'Failed to save changes')\n      }\n\n      const updatedVoter = await response.json()\n      setVoter(updatedVoter)\n      setIsEditing(false)\n    } catch (err) {\n      console.error('Error saving voter:', err)\n      setError(err instanceof Error ? err.message : 'Failed to save changes')\n    } finally {\n      setSaving(false)\n    }\n  }\n\n  // EN: Cancel editing and revert changes\n  // VI: Hủy chỉnh sửa và hoàn nguyên thay đổi\n  // ES: Cancelar edición y revertir cambios\n  const handleCancel = () => {\n    setEditForm(voter || {})\n    setIsEditing(false)\n    setError('')\n  }\n\n  // EN: Format contact type for display\n  // VI: Định dạng loại liên lạc để hiển thị\n  // ES: Formatear tipo de contacto para mostrar\n  const formatContactType = (type: string) => {\n    const typeMap = {\n      call: 'Phone Call',\n      email: 'Email',\n      visit: 'Door Visit',\n      event: 'Event'\n    }\n    return typeMap[type as keyof typeof typeMap] || type\n  }\n\n  // EN: Get status badge color\n  // VI: Lấy màu badge trạng thái\n  // ES: Obtener color de insignia de estado\n  const getStatusBadgeVariant = (status: string) => {\n    switch (status) {\n      case 'registered': return 'default'\n      case 'pending': return 'secondary'\n      case 'unregistered': return 'destructive'\n      default: return 'outline'\n    }\n  }\n\n  if (loading) {\n    return (\n      <div className=\"flex items-center justify-center min-h-screen\">\n        <div className=\"text-center\">\n          <div className=\"animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4\"></div>\n          <p>Loading voter details...</p>\n        </div>\n      </div>\n    )\n  }\n\n  if (!voter) {\n    return (\n      <div className=\"flex items-center justify-center min-h-screen\">\n        <Card className=\"w-full max-w-md\">\n          <CardContent className=\"pt-6\">\n            <div className=\"text-center\">\n              <User className=\"h-12 w-12 text-muted-foreground mx-auto mb-4\" />\n              <h2 className=\"text-xl font-semibold mb-2\">Voter Not Found</h2>\n              <p className=\"text-muted-foreground mb-4\">\n                The requested voter could not be found.\n              </p>\n              <Button onClick={() => router.back()}>\n                <ArrowLeft className=\"h-4 w-4 mr-2\" />\n                Go Back\n              </Button>\n            </div>\n          </CardContent>\n        </Card>\n      </div>\n    )\n  }\n\n  return (\n    <div className=\"container mx-auto px-4 py-6 max-w-4xl\">\n      {/* EN: Header section with navigation and actions */}\n      {/* VI: Phần header với điều hướng và hành động */}\n      {/* ES: Sección de encabezado con navegación y acciones */}\n      <div className=\"flex items-center justify-between mb-6\">\n        <div className=\"flex items-center gap-4\">\n          <Button\n            variant=\"ghost\"\n            size=\"sm\"\n            onClick={() => router.back()}\n          >\n            <ArrowLeft className=\"h-4 w-4 mr-2\" />\n            Back\n          </Button>\n          <div>\n            <h1 className=\"text-2xl font-bold\">\n              {voter.firstName} {voter.lastName}\n            </h1>\n            <p className=\"text-muted-foreground\">Voter Details</p>\n          </div>\n        </div>\n        \n        <div className=\"flex gap-2\">\n          {isEditing ? (\n            <>\n              <Button\n                variant=\"outline\"\n                size=\"sm\"\n                onClick={handleCancel}\n                disabled={saving}\n              >\n                <X className=\"h-4 w-4 mr-2\" />\n                Cancel\n              </Button>\n              <Button\n                size=\"sm\"\n                onClick={handleSave}\n                disabled={saving}\n              >\n                <Save className=\"h-4 w-4 mr-2\" />\n                {saving ? 'Saving...' : 'Save'}\n              </Button>\n            </>\n          ) : (\n            <Button\n              size=\"sm\"\n              onClick={() => setIsEditing(true)}\n            >\n              <Edit className=\"h-4 w-4 mr-2\" />\n              Edit\n            </Button>\n          )}\n        </div>\n      </div>\n\n      {error && (\n        <div className=\"bg-destructive/15 text-destructive px-4 py-3 rounded-md mb-6\">\n          {error}\n        </div>\n      )}\n\n      <div className=\"grid gap-6 md:grid-cols-2\">\n        {/* EN: Personal Information Card */}\n        {/* VI: Card thông tin cá nhân */}\n        {/* ES: Tarjeta de información personal */}\n        <Card>\n          <CardHeader>\n            <CardTitle className=\"flex items-center gap-2\">\n              <User className=\"h-5 w-5\" />\n              Personal Information\n            </CardTitle>\n          </CardHeader>\n          <CardContent 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={editForm.firstName || ''}\n                    onChange={(e) => handleInputChange('firstName', e.target.value)}\n                  />\n                ) : (\n                  <p className=\"mt-1 text-sm\">{voter.firstName}</p>\n                )}\n              </div>\n              <div>\n                <Label htmlFor=\"lastName\">Last Name</Label>\n                {isEditing ? (\n                  <Input\n                    id=\"lastName\"\n                    value={editForm.lastName || ''}\n                    onChange={(e) => handleInputChange('lastName', e.target.value)}\n                  />\n                ) : (\n                  <p className=\"mt-1 text-sm\">{voter.lastName}</p>\n                )}\n              </div>\n            </div>\n\n            <div>\n              <Label>Voter Status</Label>\n              <div className=\"mt-1\">\n                <Badge variant={getStatusBadgeVariant(voter.voterStatus)}>\n                  {voter.voterStatus.charAt(0).toUpperCase() + voter.voterStatus.slice(1)}\n                </Badge>\n              </div>\n            </div>\n\n            <div>\n              <Label>Political Affiliation</Label>\n              <p className=\"mt-1 text-sm capitalize\">{voter.politicalAffiliation}</p>\n            </div>\n          </CardContent>\n        </Card>\n\n        {/* EN: Contact Information Card */}\n        {/* VI: Card thông tin liên lạc */}\n        {/* ES: Tarjeta de información de contacto */}\n        <Card>\n          <CardHeader>\n            <CardTitle className=\"flex items-center gap-2\">\n              <Phone className=\"h-5 w-5\" />\n              Contact Information\n            </CardTitle>\n          </CardHeader>\n          <CardContent className=\"space-y-4\">\n            <div>\n              <Label htmlFor=\"email\" className=\"flex items-center gap-2\">\n                <Mail className=\"h-4 w-4\" />\n                Email\n              </Label>\n              {isEditing ? (\n                <Input\n                  id=\"email\"\n                  type=\"email\"\n                  value={editForm.email || ''}\n                  onChange={(e) => handleInputChange('email', e.target.value)}\n                />\n              ) : (\n                <p className=\"mt-1 text-sm\">{voter.email || 'Not provided'}</p>\n              )}\n            </div>\n\n            <div>\n              <Label htmlFor=\"phone\">Phone</Label>\n              {isEditing ? (\n                <Input\n                  id=\"phone\"\n                  value={editForm.phone || ''}\n                  onChange={(e) => handleInputChange('phone', e.target.value)}\n                />\n              ) : (\n                <p className=\"mt-1 text-sm\">{voter.phone || 'Not provided'}</p>\n              )}\n            </div>\n\n            <div>\n              <Label htmlFor=\"address\" className=\"flex items-center gap-2\">\n                <MapPin className=\"h-4 w-4\" />\n                Address\n              </Label>\n              {isEditing ? (\n                <Textarea\n                  id=\"address\"\n                  value={editForm.address || ''}\n                  onChange={(e) => handleInputChange('address', e.target.value)}\n                  rows={3}\n                />\n              ) : (\n                <p className=\"mt-1 text-sm\">{voter.address || 'Not provided'}</p>\n              )}\n            </div>\n          </CardContent>\n        </Card>\n      </div>\n\n      {/* EN: Notes Section */}\n      {/* VI: Phần ghi chú */}\n      {/* ES: Sección de notas */}\n      <Card className=\"mt-6\">\n        <CardHeader>\n          <CardTitle>Notes</CardTitle>\n        </CardHeader>\n        <CardContent>\n          {isEditing ? (\n            <Textarea\n              value={editForm.notes || ''}\n              onChange={(e) => handleInputChange('notes', e.target.value)}\n              placeholder=\"Add notes about this voter...\"\n              rows={4}\n            />\n          ) : (\n            <p className=\"text-sm\">\n              {voter.notes || 'No notes available'}\n            </p>\n          )}\n        </CardContent>\n      </Card>\n\n      {/* EN: Contact History Section */}\n      {/* VI: Phần lịch sử liên lạc */}\n      {/* ES: Sección de historial de contacto */}\n      <Card className=\"mt-6\">\n        <CardHeader>\n          <CardTitle className=\"flex items-center gap-2\">\n            <Calendar className=\"h-5 w-5\" />\n            Contact History\n          </CardTitle>\n        </CardHeader>\n        <CardContent>\n          {contactHistory.length === 0 ? (\n            <p className=\"text-muted-foreground text-center 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-1\">\n                      <div className=\"flex items-center gap-2 mb-1\">\n                        <Badge variant=\"outline\">\n                          {formatContactType(contact.type)}\n                        </Badge>\n                        <Badge variant={contact.outcome === 'positive' ? 'default' : \n                                      contact.outcome === 'negative' ? 'destructive' : 'secondary'}>\n                          {contact.outcome}\n                        </Badge>\n                      </div>\n                      <p className=\"text-sm text-muted-foreground mb-2\">\n                        {new Date(contact.date).toLocaleDateString()} by {contact.userName}\n                      </p>\n                      <p className=\"text-sm\">{contact.notes}</p>\n                    </div>\n                  </div>\n                  {index < contactHistory.length - 1 && (\n                    <Separator className=\"mt-4\" />\n                  )}\n                </div>\n              ))}\n            </div>\n          )}\n        </CardContent>\n      </Card>\n    </div>\n  )\n}\n```\n\nThis VoterDetailScreen component provides:\n\n1. **Complete voter detail view** with personal and contact information\n2. **Edit mode functionality** with `isEditing` state management\n3. **Full contact history** display with filtering and formatting\n4. **Form validation** using Zod schemas\n5. **Error handling** and loading states\n6. **Responsive design** with proper card layouts\n7. **Trilingual comments** following codebase conventions\n8. **SPEC section references** for CP-WIRE-02 and CP-API-01\n9. **Proper TypeScript types** and safe parsing\n10. **UI components** from the design system\n\nThe component handles both viewing and editing modes, manages form state properly, and integrates with the API endpoints for voter data and contact history."
}
Task ID: 13c2fc71-7e5f-40b5-8ac2-0d78a6d49c62