Task editing: reusable TaskEditModal, expanded PATCH API, edit button on all task surfaces
This commit is contained in:
parent
8c5516f353
commit
04c87f111a
5 changed files with 532 additions and 6 deletions
|
|
@ -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<Set<string>>(new Set())
|
||||
const [assignTo, setAssignTo] = useState('')
|
||||
const [assigning, setAssigning] = useState(false)
|
||||
const [editingTask, setEditingTask] = useState<any | null>(null)
|
||||
|
||||
// Generate & assign state
|
||||
const [genAdvocate, setGenAdvocate] = useState('')
|
||||
|
|
@ -333,6 +335,7 @@ export function BulkAssignClient({ users, clients, designations }: BulkAssignCli
|
|||
<TableHead>Due Date</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Assigned To</TableHead>
|
||||
<TableHead className="w-10"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
|
@ -382,11 +385,33 @@ export function BulkAssignClient({ users, clients, designations }: BulkAssignCli
|
|||
: <span className="text-muted-foreground">Unassigned</span>
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
onClick={() => setEditingTask(task)}
|
||||
className="inline-flex items-center text-muted-foreground hover:text-foreground transition-colors p-1 rounded hover:bg-muted"
|
||||
title="Edit task"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
|
||||
{/* Task edit modal */}
|
||||
{editingTask && (
|
||||
<TaskEditModal
|
||||
open={!!editingTask}
|
||||
onOpenChange={(open) => { if (!open) setEditingTask(null) }}
|
||||
task={editingTask as EditableTask}
|
||||
onUpdated={() => {
|
||||
setEditingTask(null)
|
||||
fetchTasks()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<boolean | null>(null)
|
||||
const [reminderDate, setReminderDate] = useState('')
|
||||
|
|
@ -286,6 +288,15 @@ function TaskCard({ task: initial }: { task: Task }) {
|
|||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setEditOpen(true)}
|
||||
className="gap-1.5 text-muted-foreground hover:text-foreground"
|
||||
title="Edit task"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isCompleted ? 'outline' : 'default'}
|
||||
|
|
@ -390,6 +401,16 @@ function TaskCard({ task: initial }: { task: Task }) {
|
|||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Edit modal */}
|
||||
<TaskEditModal
|
||||
open={editOpen}
|
||||
onOpenChange={setEditOpen}
|
||||
task={task as unknown as EditableTask}
|
||||
onUpdated={(updated) => {
|
||||
setTask((prev) => ({ ...prev, ...updated }))
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Notes panel */}
|
||||
{noteOpen && (
|
||||
<div className="mt-3 pt-3 border-t border-border space-y-3">
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@ import { prisma } from '@/lib/db'
|
|||
|
||||
/**
|
||||
* PATCH /api/tasks/[id]
|
||||
* Update a task's status and/or notes.
|
||||
* Body: { status?: 'COMPLETED' | 'NOT_STARTED', notes?: string }
|
||||
* Update any editable fields on a task.
|
||||
* Any authenticated user who is assigned to the task (or Admin/Manager) may update it.
|
||||
*/
|
||||
export async function PATCH(
|
||||
|
|
@ -39,7 +38,11 @@ export async function PATCH(
|
|||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { status, notes, naReason, imageRightFiled, reminderDate } = body
|
||||
const {
|
||||
status, notes, naReason, imageRightFiled, reminderDate,
|
||||
title, description, priority, department, dueDate,
|
||||
clientId, policyId, policyGroupId, assignedUserIds,
|
||||
} = body
|
||||
|
||||
const allowedStatuses = ['COMPLETED', 'NOT_STARTED', 'IN_PROGRESS', 'BLOCKED', 'NA', 'CANCELLED']
|
||||
if (status && !allowedStatuses.includes(status)) {
|
||||
|
|
@ -51,6 +54,8 @@ export async function PATCH(
|
|||
}
|
||||
|
||||
const updateData: any = {}
|
||||
|
||||
// Status changes
|
||||
if (status !== undefined) {
|
||||
updateData.status = status
|
||||
if (status === 'COMPLETED') {
|
||||
|
|
@ -64,15 +69,66 @@ export async function PATCH(
|
|||
updateData.naReason = naReason.trim()
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar field updates
|
||||
if (title !== undefined) updateData.title = title
|
||||
if (description !== undefined) updateData.description = description || null
|
||||
if (priority !== undefined) updateData.priority = priority
|
||||
if (department !== undefined) updateData.department = department
|
||||
if (dueDate !== undefined) updateData.dueDate = new Date(dueDate)
|
||||
if (notes !== undefined) updateData.notes = notes
|
||||
if (imageRightFiled !== undefined) updateData.imageRightFiled = imageRightFiled
|
||||
if (reminderDate !== undefined) updateData.reminderDate = reminderDate ? new Date(reminderDate) : null
|
||||
|
||||
// Association changes
|
||||
if (clientId !== undefined) updateData.clientId = clientId
|
||||
if (policyId !== undefined) updateData.policyId = policyId || null
|
||||
if (policyGroupId !== undefined) updateData.policyGroupId = policyGroupId || null
|
||||
|
||||
const updated = await prisma.task.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
client: { select: { id: true, name: true } },
|
||||
policy: { select: { id: true, policyNumber: true, policyType: true, expirationDate: true } },
|
||||
policyGroup: { select: { id: true, name: true, renewalDate: true } },
|
||||
assignments: { include: { user: { select: { id: true, displayName: true, email: true } } } },
|
||||
},
|
||||
})
|
||||
|
||||
// Reassign users if provided
|
||||
if (Array.isArray(assignedUserIds)) {
|
||||
await prisma.taskAssignment.deleteMany({ where: { taskId: id } })
|
||||
if (assignedUserIds.length > 0) {
|
||||
await prisma.taskAssignment.createMany({
|
||||
data: assignedUserIds.map((uid: string) => ({ taskId: id, userId: uid })),
|
||||
})
|
||||
}
|
||||
// Re-fetch with updated assignments
|
||||
const refreshed = await prisma.task.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
client: { select: { id: true, name: true } },
|
||||
policy: { select: { id: true, policyNumber: true, policyType: true, expirationDate: true } },
|
||||
policyGroup: { select: { id: true, name: true, renewalDate: true } },
|
||||
assignments: { include: { user: { select: { id: true, displayName: true, email: true } } } },
|
||||
},
|
||||
})
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId,
|
||||
action: 'TASK_UPDATED',
|
||||
entityType: 'Task',
|
||||
entityId: id,
|
||||
oldValues: { status: task.status, notes: task.notes },
|
||||
newValues: { ...updateData, assignedUserIds },
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json(refreshed)
|
||||
}
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
userId,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { Combobox, type ComboboxGroup } from '@/components/ui/combobox'
|
|||
import { formatDate, formatRenewalDate } from '@/lib/utils'
|
||||
import { PolicyGroupManager } from '@/components/clients/policy-group-manager'
|
||||
import { AdditionalServiceModal } from '@/components/tasks/additional-service-modal'
|
||||
import { TaskEditModal, type EditableTask } from '@/components/tasks/task-edit-modal'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface SimpleUser {
|
||||
|
|
@ -68,6 +69,7 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
|
|||
const [showCompleted, setShowCompleted] = useState(false)
|
||||
const [notesExpanded, setNotesExpanded] = useState(!!(client.notes))
|
||||
const [additionalServiceOpen, setAdditionalServiceOpen] = useState(false)
|
||||
const [editingTaskId, setEditingTaskId] = useState<string | null>(null)
|
||||
const [sessionUserId, setSessionUserId] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -711,7 +713,15 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
|
|||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right shrink-0 ml-4">
|
||||
<div className="flex flex-col items-end gap-1 shrink-0 ml-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditingTaskId(task.id)}
|
||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground hover:bg-muted rounded px-1.5 py-0.5 transition-colors"
|
||||
title="Edit task"
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</button>
|
||||
<p className={`text-sm font-medium ${overdue ? 'text-red-500' : ''}`} suppressHydrationWarning>
|
||||
Due: {formatDate(task.dueDate)}
|
||||
</p>
|
||||
|
|
@ -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 (
|
||||
<TaskEditModal
|
||||
open={!!editingTaskId}
|
||||
onOpenChange={(open) => { if (!open) setEditingTaskId(null) }}
|
||||
task={{
|
||||
...editTask,
|
||||
clientId: client.id,
|
||||
client: { id: client.id, name: client.name },
|
||||
} as EditableTask}
|
||||
onUpdated={() => {
|
||||
setEditingTaskId(null)
|
||||
refreshTasks()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
</TabsContent>
|
||||
|
||||
<AdditionalServiceModal
|
||||
|
|
|
|||
393
ondeck/src/components/tasks/task-edit-modal.tsx
Normal file
393
ondeck/src/components/tasks/task-edit-modal.tsx
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { UserSelectContent } from '@/components/ui/user-select-content'
|
||||
import { Pencil } from 'lucide-react'
|
||||
|
||||
interface SimpleUser {
|
||||
id: string
|
||||
displayName: string | null
|
||||
email: string
|
||||
department?: string | null
|
||||
}
|
||||
|
||||
interface PolicyOption {
|
||||
id: string
|
||||
policyNumber: string | null
|
||||
policyType: string | null
|
||||
}
|
||||
|
||||
interface PolicyGroupOption {
|
||||
id: string
|
||||
name?: string | null
|
||||
renewalDate?: string | null
|
||||
}
|
||||
|
||||
export interface EditableTask {
|
||||
id: string
|
||||
title: string
|
||||
description: string | null
|
||||
priority: string
|
||||
department: string
|
||||
dueDate: string | Date
|
||||
status: string
|
||||
clientId: string
|
||||
policyId?: string | null
|
||||
policyGroupId?: string | null
|
||||
client?: { id: string; name: string } | null
|
||||
policy?: { id: string; policyNumber: string | null; policyType: string | null; expirationDate?: string | Date | null } | null
|
||||
policyGroup?: { id: string; name: string | null; renewalDate?: string | Date | null } | null
|
||||
assignments: Array<{ id?: string; user: { id?: string; displayName: string | null; email: string }; userId?: string }>
|
||||
}
|
||||
|
||||
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<SimpleUser[]>([])
|
||||
|
||||
// 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<PolicyOption[]>([])
|
||||
const [groups, setGroups] = useState<PolicyGroupOption[]>([])
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Pencil className="h-5 w-5" />
|
||||
Edit Task
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-2">
|
||||
{/* Title */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium">Title *</label>
|
||||
<Input value={title} onChange={(e) => setTitle(e.target.value)} />
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium">Description</label>
|
||||
<Textarea value={description} onChange={(e) => setDescription(e.target.value)} rows={2} className="text-sm" />
|
||||
</div>
|
||||
|
||||
{/* Client */}
|
||||
<div className="space-y-1.5 relative">
|
||||
<label className="text-sm font-medium">Client *</label>
|
||||
<Input
|
||||
placeholder="Search client..."
|
||||
value={clientSearch}
|
||||
onChange={(e) => { setClientSearch(e.target.value); setClientId(''); setClientDropOpen(true) }}
|
||||
readOnly={!!clientId}
|
||||
/>
|
||||
{clientId && (
|
||||
<button
|
||||
onClick={() => { setClientId(''); setClientSearch(''); setPolicies([]); setGroups([]) }}
|
||||
className="absolute right-2 top-8 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
{clientDropOpen && clientOptions.length > 0 && !clientId && (
|
||||
<div className="absolute z-50 mt-1 w-full rounded-md border border-border bg-popover shadow-md">
|
||||
{clientOptions.map((c) => (
|
||||
<button
|
||||
key={c.id}
|
||||
onClick={() => {
|
||||
setClientId(c.id)
|
||||
setClientSearch(c.name)
|
||||
setClientDropOpen(false)
|
||||
fetchClientData(c.id)
|
||||
}}
|
||||
className="w-full px-3 py-2 text-sm hover:bg-muted text-left"
|
||||
>
|
||||
{c.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Level */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium">Associate at level</label>
|
||||
<div className="flex gap-2">
|
||||
{(['client', 'policy', 'group'] as const).map((l) => (
|
||||
<button
|
||||
key={l}
|
||||
onClick={() => setLevel(l)}
|
||||
className={`flex-1 py-1.5 rounded-md border text-sm capitalize transition-colors ${
|
||||
level === l ? 'bg-primary text-primary-foreground border-primary' : 'border-border hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
{l === 'group' ? 'Policy Group' : l === 'policy' ? 'Policy' : 'Client'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Policy selector */}
|
||||
{level === 'policy' && policies.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium">Policy</label>
|
||||
<Select value={selectedPolicyId} onValueChange={setSelectedPolicyId}>
|
||||
<SelectTrigger><SelectValue placeholder="Select policy..." /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{policies.map((p) => (
|
||||
<SelectItem key={p.id} value={p.id}>
|
||||
{[p.policyNumber, p.policyType].filter(Boolean).join(' · ')}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Policy Group selector */}
|
||||
{level === 'group' && groups.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium">Policy Group</label>
|
||||
<Select value={selectedGroupId} onValueChange={setSelectedGroupId}>
|
||||
<SelectTrigger><SelectValue placeholder="Select group..." /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{groups.map((g) => (
|
||||
<SelectItem key={g.id} value={g.id}>
|
||||
{g.name ?? g.renewalDate ?? g.id}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Priority + Department row */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium">Priority</label>
|
||||
<Select value={priority} onValueChange={setPriority}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{PRIORITIES.map((p) => <SelectItem key={p} value={p}>{p.charAt(0) + p.slice(1).toLowerCase()}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium">Department</label>
|
||||
<Select value={department} onValueChange={setDepartment}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{DEPARTMENTS.map((d) => <SelectItem key={d.value} value={d.value}>{d.label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Due date */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium">Due Date *</label>
|
||||
<input
|
||||
type="date"
|
||||
value={dueDate}
|
||||
onChange={(e) => setDueDate(e.target.value)}
|
||||
className="w-full h-9 rounded-md border border-input bg-background px-3 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Assign to */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium">Assign to</label>
|
||||
<Select value={assignTo} onValueChange={setAssignTo}>
|
||||
<SelectTrigger><SelectValue placeholder="Select user..." /></SelectTrigger>
|
||||
<UserSelectContent users={users} />
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>Cancel</Button>
|
||||
<Button onClick={handleSubmit} disabled={saving} className="gap-1.5">
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue