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
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