From 04c87f111a98f8183a75aa6b03c790c99b5dce7c Mon Sep 17 00:00:00 2001 From: lorentz Date: Fri, 10 Apr 2026 14:20:57 +0000 Subject: [PATCH] Task editing: reusable TaskEditModal, expanded PATCH API, edit button on all task surfaces --- .../(dashboard)/tasks/assign/page-client.tsx | 27 +- .../src/app/(dashboard)/tasks/page-client.tsx | 23 +- ondeck/src/app/api/tasks/[id]/route.ts | 62 ++- .../src/components/clients/client-detail.tsx | 33 +- .../src/components/tasks/task-edit-modal.tsx | 393 ++++++++++++++++++ 5 files changed, 532 insertions(+), 6 deletions(-) create mode 100644 ondeck/src/components/tasks/task-edit-modal.tsx diff --git a/ondeck/src/app/(dashboard)/tasks/assign/page-client.tsx b/ondeck/src/app/(dashboard)/tasks/assign/page-client.tsx index c26c478..04218f2 100644 --- a/ondeck/src/app/(dashboard)/tasks/assign/page-client.tsx +++ b/ondeck/src/app/(dashboard)/tasks/assign/page-client.tsx @@ -22,8 +22,9 @@ import { TableHeader, TableRow, } from '@/components/ui/table' -import { Users, Filter, CheckSquare, Wand2, MessageSquare } from 'lucide-react' +import { Users, Filter, CheckSquare, Wand2, MessageSquare, Pencil } from 'lucide-react' import { formatDate, formatRenewalDate } from '@/lib/utils' +import { TaskEditModal, type EditableTask } from '@/components/tasks/task-edit-modal' interface SimpleUser { id: string; displayName: string | null; email: string; department: string | null } interface SimpleClient { id: string; name: string } @@ -66,6 +67,7 @@ export function BulkAssignClient({ users, clients, designations }: BulkAssignCli const [selected, setSelected] = useState>(new Set()) const [assignTo, setAssignTo] = useState('') const [assigning, setAssigning] = useState(false) + const [editingTask, setEditingTask] = useState(null) // Generate & assign state const [genAdvocate, setGenAdvocate] = useState('') @@ -333,6 +335,7 @@ export function BulkAssignClient({ users, clients, designations }: BulkAssignCli Due Date Status Assigned To + @@ -382,11 +385,33 @@ export function BulkAssignClient({ users, clients, designations }: BulkAssignCli : Unassigned } + e.stopPropagation()}> + + ))} )} + + {/* Task edit modal */} + {editingTask && ( + { if (!open) setEditingTask(null) }} + task={editingTask as EditableTask} + onUpdated={() => { + setEditingTask(null) + fetchTasks() + }} + /> + )} diff --git a/ondeck/src/app/(dashboard)/tasks/page-client.tsx b/ondeck/src/app/(dashboard)/tasks/page-client.tsx index 1b50266..ec9e237 100644 --- a/ondeck/src/app/(dashboard)/tasks/page-client.tsx +++ b/ondeck/src/app/(dashboard)/tasks/page-client.tsx @@ -21,10 +21,11 @@ import { SelectValue, } from '@/components/ui/select' import { UserSelectContent } from '@/components/ui/user-select-content' -import { CheckSquare, Clock, AlertCircle, MessageSquare, CheckCircle2, RotateCcw, Eye, CalendarRange, ArrowRightLeft, Search, X, Building2, Plus, Ban, ArrowUpDown, ArrowUp, ArrowDown, Filter } from 'lucide-react' +import { CheckSquare, Clock, AlertCircle, MessageSquare, CheckCircle2, RotateCcw, Eye, CalendarRange, ArrowRightLeft, Search, X, Building2, Plus, Ban, ArrowUpDown, ArrowUp, ArrowDown, Filter, Pencil } from 'lucide-react' import { Input } from '@/components/ui/input' import { formatDate, formatRenewalDate } from '@/lib/utils' import { AdditionalServiceModal } from '@/components/tasks/additional-service-modal' +import { TaskEditModal, type EditableTask } from '@/components/tasks/task-edit-modal' interface TaskUser { displayName: string | null; email: string } interface TaskAssignment { id: string; user: TaskUser } @@ -95,6 +96,7 @@ function TaskCard({ task: initial }: { task: Task }) { const [naDialogOpen, setNaDialogOpen] = useState(false) const [naReason, setNaReason] = useState('') const [naSubmitting, setNaSubmitting] = useState(false) + const [editOpen, setEditOpen] = useState(false) const [completeDialogOpen, setCompleteDialogOpen] = useState(false) const [imageRightFiled, setImageRightFiled] = useState(null) const [reminderDate, setReminderDate] = useState('') @@ -286,6 +288,15 @@ function TaskCard({ task: initial }: { task: Task }) { {/* Action buttons */}
+

Due: {formatDate(task.dueDate)}

@@ -723,6 +733,27 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic }) ) })()} + + {/* Task edit modal */} + {editingTaskId && (() => { + const editTask = tasks.find((t: any) => t.id === editingTaskId) + if (!editTask) return null + return ( + { if (!open) setEditingTaskId(null) }} + task={{ + ...editTask, + clientId: client.id, + client: { id: client.id, name: client.name }, + } as EditableTask} + onUpdated={() => { + setEditingTaskId(null) + refreshTasks() + }} + /> + ) + })()} +} + +interface TaskEditModalProps { + open: boolean + onOpenChange: (open: boolean) => void + task: EditableTask + onUpdated: (updated: any) => void +} + +const PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] +const DEPARTMENTS = [ + { value: 'CLAIMS', label: 'Claims' }, + { value: 'PERSONAL_LINES', label: 'Personal Lines' }, + { value: 'COMMERCIAL_LINES', label: 'Commercial Lines' }, + { value: 'BENEFITS', label: 'Benefits' }, + { value: 'OTHER', label: 'Other' }, +] + +export function TaskEditModal({ open, onOpenChange, task, onUpdated }: TaskEditModalProps) { + const [title, setTitle] = useState('') + const [description, setDescription] = useState('') + const [priority, setPriority] = useState('MEDIUM') + const [department, setDepartment] = useState('CLAIMS') + const [dueDate, setDueDate] = useState('') + const [assignTo, setAssignTo] = useState('') + const [level, setLevel] = useState<'client' | 'policy' | 'group'>('client') + const [saving, setSaving] = useState(false) + + const [users, setUsers] = useState([]) + + // Client search + const [clientSearch, setClientSearch] = useState('') + const [clientId, setClientId] = useState('') + const [clientOptions, setClientOptions] = useState<{ id: string; name: string }[]>([]) + const [clientDropOpen, setClientDropOpen] = useState(false) + + // Policy/group lists for the selected client + const [policies, setPolicies] = useState([]) + const [groups, setGroups] = useState([]) + const [selectedPolicyId, setSelectedPolicyId] = useState('') + const [selectedGroupId, setSelectedGroupId] = useState('') + + // Populate form from task when modal opens + useEffect(() => { + if (!open) return + + setTitle(task.title) + setDescription(task.description ?? '') + setPriority(task.priority) + setDepartment(task.department) + setDueDate(new Date(task.dueDate).toISOString().split('T')[0]) + setClientId(task.clientId || task.client?.id || '') + setClientSearch(task.client?.name || '') + + const firstAssigneeId = task.assignments?.[0]?.user?.id || task.assignments?.[0]?.userId || '' + setAssignTo(firstAssigneeId as string) + + if (task.policyGroupId || task.policyGroup) { + setLevel('group') + setSelectedGroupId(task.policyGroupId || task.policyGroup?.id || '') + setSelectedPolicyId('') + } else if (task.policyId || task.policy) { + setLevel('policy') + setSelectedPolicyId(task.policyId || task.policy?.id || '') + setSelectedGroupId('') + } else { + setLevel('client') + setSelectedPolicyId('') + setSelectedGroupId('') + } + + fetch('/api/users?isActive=true&limit=200') + .then((r) => r.json()) + .then((d) => setUsers(d.users || [])) + .catch(() => {}) + + // Fetch policies/groups for the client + const cid = task.clientId || task.client?.id + if (cid) fetchClientData(cid) + }, [open]) + + async function fetchClientData(cid: string) { + try { + const [clientRes, groupsRes] = await Promise.all([ + fetch(`/api/clients/${cid}`), + fetch(`/api/clients/${cid}/policy-groups`), + ]) + if (clientRes.ok) { + const cd = await clientRes.json() + const pols = (cd.policies || []).map((p: any) => ({ + id: p.id, + policyNumber: p.policyNumber, + policyType: p.policyType, + })) + setPolicies(pols) + } + if (groupsRes.ok) { + const gd = await groupsRes.json() + const grps = (Array.isArray(gd) ? gd : gd.groups || []).map((g: any) => ({ + id: g.id, + name: g.name, + renewalDate: g.renewalDate, + })) + setGroups(grps) + } + } catch {} + } + + // Client search debounce + useEffect(() => { + if (!clientSearch.trim() || clientId) { setClientOptions([]); return } + const t = setTimeout(async () => { + try { + const res = await fetch(`/api/clients?search=${encodeURIComponent(clientSearch)}&limit=20`) + const data = await res.json() + setClientOptions((data.clients || []).map((c: any) => ({ id: c.id, name: c.name }))) + setClientDropOpen(true) + } catch {} + }, 250) + return () => clearTimeout(t) + }, [clientSearch, clientId]) + + const handleSubmit = async () => { + if (!title.trim()) { toast.error('Title is required'); return } + if (!dueDate) { toast.error('Due date is required'); return } + if (!clientId) { toast.error('Please select a client'); return } + + setSaving(true) + try { + const body: any = { + title: title.trim(), + description: description.trim() || null, + priority, + department, + dueDate: new Date(dueDate).toISOString(), + clientId, + } + + if (level === 'policy' && selectedPolicyId) { + body.policyId = selectedPolicyId + body.policyGroupId = null + } else if (level === 'group' && selectedGroupId) { + body.policyGroupId = selectedGroupId + body.policyId = null + } else { + body.policyId = null + body.policyGroupId = null + } + + if (assignTo) body.assignedUserIds = [assignTo] + + const res = await fetch(`/api/tasks/${task.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + const data = await res.json() + if (!res.ok) throw new Error(data.error) + + toast.success('Task updated') + onUpdated(data) + onOpenChange(false) + } catch (err: any) { + toast.error(err.message || 'Failed to update task') + } finally { + setSaving(false) + } + } + + return ( + + + + + + Edit Task + + + +
+ {/* Title */} +
+ + setTitle(e.target.value)} /> +
+ + {/* Description */} +
+ +