Additional Service: ad hoc task creation on tasks page, client detail, policy group manager
This commit is contained in:
parent
c5c3d5a4ab
commit
0d8edc97da
9 changed files with 539 additions and 8 deletions
|
|
@ -291,6 +291,7 @@ model Task {
|
|||
completedAt DateTime? @map("completed_at")
|
||||
completedBy String? @map("completed_by")
|
||||
notes String? @db.Text
|
||||
isAdHoc Boolean @default(false) @map("is_ad_hoc")
|
||||
naReason String? @map("na_reason") @db.Text
|
||||
cancelledReason String? @map("cancelled_reason") @db.Text
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { CheckSquare, Clock, AlertCircle, MessageSquare, CheckCircle2, RotateCcw, Eye, CalendarRange, ArrowRightLeft, Search, X, Building2 } from 'lucide-react'
|
||||
import { CheckSquare, Clock, AlertCircle, MessageSquare, CheckCircle2, RotateCcw, Eye, CalendarRange, ArrowRightLeft, Search, X, Building2, Plus } from 'lucide-react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import { AdditionalServiceModal } from '@/components/tasks/additional-service-modal'
|
||||
|
||||
interface TaskUser { displayName: string | null; email: string }
|
||||
interface TaskAssignment { id: string; user: TaskUser }
|
||||
|
|
@ -290,6 +291,7 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users }
|
|||
const [transferOpen, setTransferOpen] = useState(false)
|
||||
const [transferTo, setTransferTo] = useState('')
|
||||
const [transferring, setTransferring] = useState(false)
|
||||
const [additionalServiceOpen, setAdditionalServiceOpen] = useState(false)
|
||||
const [viewingUserId, setViewingUserId] = useState(currentUserId)
|
||||
const [loadingTasks, setLoadingTasks] = useState(false)
|
||||
const [clientFilter, setClientFilter] = useState('')
|
||||
|
|
@ -420,8 +422,17 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users }
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0 flex-wrap">
|
||||
<Button
|
||||
size="sm"
|
||||
className="gap-1.5"
|
||||
onClick={() => setAdditionalServiceOpen(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4" /> Additional Service
|
||||
</Button>
|
||||
|
||||
{isPrivileged && (
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap">Viewing as:</span>
|
||||
<Select value={viewingUserId} onValueChange={handleUserChange}>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
|
|
@ -458,6 +469,7 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users }
|
|||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Client filter */}
|
||||
|
|
@ -729,6 +741,13 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users }
|
|||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AdditionalServiceModal
|
||||
open={additionalServiceOpen}
|
||||
onOpenChange={setAdditionalServiceOpen}
|
||||
currentUserId={currentUserId}
|
||||
onCreated={() => fetchTasks(viewingUserId, clientFilter || undefined)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
26
ondeck/src/app/api/tasks/adhoc-recent/route.ts
Normal file
26
ondeck/src/app/api/tasks/adhoc-recent/route.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/db'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session?.user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
|
||||
const userId = (session.user as any).id
|
||||
|
||||
const recent = await prisma.task.findMany({
|
||||
where: { isAdHoc: true, createdBy: userId },
|
||||
select: { id: true, title: true, description: true, priority: true, department: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 10,
|
||||
distinct: ['title'],
|
||||
})
|
||||
|
||||
return NextResponse.json(recent)
|
||||
} catch (error) {
|
||||
console.error('adhoc-recent error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -114,14 +114,16 @@ export async function POST(request: NextRequest) {
|
|||
}
|
||||
|
||||
const body = await request.json()
|
||||
const { clientId, policyId, templateId, assignedUserIds, ...taskData } = body
|
||||
const { clientId, policyId, policyGroupId, templateId, isAdHoc, assignedUserIds, ...taskData } = body
|
||||
|
||||
const task = await prisma.task.create({
|
||||
data: {
|
||||
...taskData,
|
||||
clientId,
|
||||
policyId,
|
||||
policyGroupId,
|
||||
templateId,
|
||||
isAdHoc: isAdHoc ?? false,
|
||||
createdBy: (session.user as any).id,
|
||||
assignments: assignedUserIds
|
||||
? {
|
||||
|
|
|
|||
|
|
@ -80,12 +80,12 @@ export function ClientCard({ client }: ClientCardProps) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Next Policy Expiration */}
|
||||
{/* Renewal Date */}
|
||||
{nextPolicy && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Next expiration:</span>
|
||||
<span className="text-muted-foreground">Renewal date:</span>
|
||||
<span className="font-medium" suppressHydrationWarning>
|
||||
{formatDate(nextPolicy.expirationDate)}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { Building2, MapPin, Phone, Mail, FileText, CheckSquare, CalendarRange, U
|
|||
import { Combobox } from '@/components/ui/combobox'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import { PolicyGroupManager } from '@/components/clients/policy-group-manager'
|
||||
import { AdditionalServiceModal } from '@/components/tasks/additional-service-modal'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface SimpleUser {
|
||||
|
|
@ -37,6 +38,15 @@ export function ClientDetail({ client, designations, policyGroups = [], canManag
|
|||
const [selectedDesignation2, setSelectedDesignation2] = useState(client.designation2Id || '')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [tasks, setTasks] = useState<any[]>(client.tasks || [])
|
||||
const [additionalServiceOpen, setAdditionalServiceOpen] = useState(false)
|
||||
const [sessionUserId, setSessionUserId] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/auth/session')
|
||||
.then((r) => r.json())
|
||||
.then((s) => setSessionUserId(s?.user?.id ?? ''))
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
const refreshTasks = async () => {
|
||||
try {
|
||||
|
|
@ -321,7 +331,7 @@ export function ClientDetail({ client, designations, policyGroups = [], canManag
|
|||
</div>
|
||||
<Badge variant={new Date(policy.expirationDate) < new Date() ? 'destructive' : 'default'}>
|
||||
<span suppressHydrationWarning>
|
||||
Expires {formatDate(policy.expirationDate)}
|
||||
Renewal {formatDate(policy.expirationDate)}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
|
|
@ -395,6 +405,11 @@ export function ClientDetail({ client, designations, policyGroups = [], canManag
|
|||
</TabsContent>
|
||||
|
||||
<TabsContent value="tasks" className="space-y-4">
|
||||
<div className="flex justify-end">
|
||||
<Button size="sm" className="gap-1.5" onClick={() => setAdditionalServiceOpen(true)}>
|
||||
<Plus className="h-4 w-4" /> Additional Service
|
||||
</Button>
|
||||
</div>
|
||||
{tasks.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="pt-6 text-center text-muted-foreground">
|
||||
|
|
@ -423,6 +438,27 @@ export function ClientDetail({ client, designations, policyGroups = [], canManag
|
|||
)}
|
||||
</TabsContent>
|
||||
|
||||
<AdditionalServiceModal
|
||||
open={additionalServiceOpen}
|
||||
onOpenChange={setAdditionalServiceOpen}
|
||||
currentUserId={sessionUserId}
|
||||
context={{
|
||||
clientId: client.id,
|
||||
clientName: client.name,
|
||||
policies: client.policies?.map((p: any) => ({
|
||||
id: p.id,
|
||||
policyNumber: p.policyNumber,
|
||||
policyType: p.policyType,
|
||||
})),
|
||||
policyGroups: policyGroups.map((g: any) => ({
|
||||
id: g.id,
|
||||
name: g.name,
|
||||
renewalDate: g.renewalDate ? new Date(g.renewalDate).toLocaleDateString() : undefined,
|
||||
})),
|
||||
}}
|
||||
onCreated={refreshTasks}
|
||||
/>
|
||||
|
||||
<TabsContent value="renewal-groups">
|
||||
<PolicyGroupManager
|
||||
clientId={client.id}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export function ClientTable({ clients }: ClientTableProps) {
|
|||
<TableHead>Client</TableHead>
|
||||
<TableHead>Location</TableHead>
|
||||
<TableHead>Designation</TableHead>
|
||||
<TableHead>Next Expiration</TableHead>
|
||||
<TableHead>Renewal Date</TableHead>
|
||||
<TableHead>Policy Type</TableHead>
|
||||
<TableHead>Carrier</TableHead>
|
||||
<TableHead>Executive</TableHead>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Plus,
|
||||
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { AdditionalServiceModal } from '@/components/tasks/additional-service-modal'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
|
@ -94,6 +95,15 @@ export function PolicyGroupManager({
|
|||
const [deleteTarget, setDeleteTarget] = useState<PolicyGroup | null>(null)
|
||||
const [cancelTasks, setCancelTasks] = useState(false)
|
||||
const [generatingFor, setGeneratingFor] = useState<string | null>(null)
|
||||
const [adhocGroupId, setAdhocGroupId] = useState<string | null>(null)
|
||||
const [sessionUserId, setSessionUserId] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/auth/session')
|
||||
.then((r) => r.json())
|
||||
.then((s) => setSessionUserId(s?.user?.id ?? ''))
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
const assignedPolicyIds = new Set(
|
||||
groups.flatMap((g) => g.policies.map((p) => p.id))
|
||||
|
|
@ -322,6 +332,14 @@ export function PolicyGroupManager({
|
|||
<div className="flex items-center gap-1 shrink-0">
|
||||
{canManage && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setAdhocGroupId(group.id)}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||
Additional Service
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
|
@ -571,6 +589,41 @@ export function PolicyGroupManager({
|
|||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{adhocGroupId && (() => {
|
||||
const grp = groups.find((g) => g.id === adhocGroupId)
|
||||
return (
|
||||
<AdditionalServiceModal
|
||||
open={!!adhocGroupId}
|
||||
onOpenChange={(open) => { if (!open) setAdhocGroupId(null) }}
|
||||
currentUserId={sessionUserId}
|
||||
context={{
|
||||
clientId: clientId,
|
||||
policyGroupId: adhocGroupId,
|
||||
policyGroups: groups.map((g) => ({
|
||||
id: g.id,
|
||||
name: g.name,
|
||||
renewalDate: g.renewalDate ? new Date(g.renewalDate).toLocaleDateString() : undefined,
|
||||
})),
|
||||
policies: grp?.policies.map((p) => ({
|
||||
id: p.id,
|
||||
policyNumber: p.policyNumber,
|
||||
policyType: p.policyType,
|
||||
})),
|
||||
}}
|
||||
onCreated={() => {
|
||||
setGroups((prev) =>
|
||||
prev.map((g) =>
|
||||
g.id === adhocGroupId
|
||||
? { ...g, _count: { tasks: g._count.tasks + 1 } }
|
||||
: g
|
||||
)
|
||||
)
|
||||
onTasksGenerated?.(1)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
394
ondeck/src/components/tasks/additional-service-modal.tsx
Normal file
394
ondeck/src/components/tasks/additional-service-modal.tsx
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
'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,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Plus, Wand2 } 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
|
||||
}
|
||||
|
||||
interface RecentAdHoc {
|
||||
id: string
|
||||
title: string
|
||||
description: string | null
|
||||
priority: string
|
||||
department: string
|
||||
}
|
||||
|
||||
interface AdditionalServiceModalProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
currentUserId: string
|
||||
/** Pre-fill context — pass what's known from the current surface */
|
||||
context?: {
|
||||
clientId?: string
|
||||
clientName?: string
|
||||
policyId?: string
|
||||
policyGroupId?: string
|
||||
policies?: PolicyOption[]
|
||||
policyGroups?: PolicyGroupOption[]
|
||||
}
|
||||
onCreated?: () => void
|
||||
}
|
||||
|
||||
const PRIORITIES = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']
|
||||
const DEPARTMENTS = ['Claims', 'Personal Lines', 'Commercial Lines', 'Benefits', 'Life', 'Other']
|
||||
const TIMINGS = ['BEFORE_RENEWAL', 'AT_RENEWAL', 'AFTER_RENEWAL', 'ONGOING']
|
||||
|
||||
export function AdditionalServiceModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
currentUserId,
|
||||
context,
|
||||
onCreated,
|
||||
}: AdditionalServiceModalProps) {
|
||||
const [level, setLevel] = useState<'client' | 'policy' | 'group'>('client')
|
||||
const [title, setTitle] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [priority, setPriority] = useState('MEDIUM')
|
||||
const [department, setDepartment] = useState(DEPARTMENTS[0])
|
||||
const [dueDate, setDueDate] = useState('')
|
||||
const [assignTo, setAssignTo] = useState(currentUserId)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
const [users, setUsers] = useState<SimpleUser[]>([])
|
||||
const [recentAdHoc, setRecentAdHoc] = useState<RecentAdHoc[]>([])
|
||||
|
||||
// Client search (when no clientId pre-filled)
|
||||
const [clientSearch, setClientSearch] = useState(context?.clientName ?? '')
|
||||
const [clientId, setClientId] = useState(context?.clientId ?? '')
|
||||
const [clientOptions, setClientOptions] = useState<{ id: string; name: string }[]>([])
|
||||
const [clientDropOpen, setClientDropOpen] = useState(false)
|
||||
|
||||
const [selectedPolicyId, setSelectedPolicyId] = useState(context?.policyId ?? '')
|
||||
const [selectedGroupId, setSelectedGroupId] = useState(context?.policyGroupId ?? '')
|
||||
|
||||
// Load users and recent ad hoc tasks when modal opens
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
// Reset form
|
||||
setTitle('')
|
||||
setDescription('')
|
||||
setPriority('MEDIUM')
|
||||
setDepartment(DEPARTMENTS[0])
|
||||
setDueDate('')
|
||||
setAssignTo(currentUserId)
|
||||
setLevel(context?.policyId ? 'policy' : context?.policyGroupId ? 'group' : 'client')
|
||||
setClientId(context?.clientId ?? '')
|
||||
setClientSearch(context?.clientName ?? '')
|
||||
setSelectedPolicyId(context?.policyId ?? '')
|
||||
setSelectedGroupId(context?.policyGroupId ?? '')
|
||||
|
||||
fetch('/api/users?isActive=true&limit=200')
|
||||
.then((r) => r.json())
|
||||
.then((d) => setUsers(d.users || []))
|
||||
.catch(() => {})
|
||||
|
||||
fetch('/api/tasks/adhoc-recent')
|
||||
.then((r) => r.json())
|
||||
.then((d) => setRecentAdHoc(Array.isArray(d) ? d : []))
|
||||
.catch(() => {})
|
||||
}, [open])
|
||||
|
||||
// Client search debounce
|
||||
useEffect(() => {
|
||||
if (context?.clientId || !clientSearch.trim()) { 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, context?.clientId])
|
||||
|
||||
const applyRecent = (r: RecentAdHoc) => {
|
||||
setTitle(r.title)
|
||||
setDescription(r.description ?? '')
|
||||
setPriority(r.priority)
|
||||
setDepartment(r.department)
|
||||
}
|
||||
|
||||
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(),
|
||||
timing: 'ONGOING',
|
||||
daysOffset: 0,
|
||||
status: 'NOT_STARTED',
|
||||
isAdHoc: true,
|
||||
clientId,
|
||||
assignedUserIds: [assignTo],
|
||||
}
|
||||
|
||||
if (level === 'policy' && selectedPolicyId) body.policyId = selectedPolicyId
|
||||
if (level === 'group' && selectedGroupId) body.policyGroupId = selectedGroupId
|
||||
|
||||
const res = await fetch('/api/tasks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw new Error(data.error)
|
||||
|
||||
toast.success('Additional service task created')
|
||||
onOpenChange(false)
|
||||
onCreated?.()
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || 'Failed to create task')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const claimsUsers = users.filter((u) => u.department?.toLowerCase().includes('claims'))
|
||||
const otherUsers = users.filter((u) => !u.department?.toLowerCase().includes('claims'))
|
||||
|
||||
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">
|
||||
<Plus className="h-5 w-5" />
|
||||
Additional Service
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-2">
|
||||
{/* Recent suggestions */}
|
||||
{recentAdHoc.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs font-medium text-muted-foreground flex items-center gap-1">
|
||||
<Wand2 className="h-3 w-3" /> Recent — click to pre-fill
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{recentAdHoc.slice(0, 5).map((r) => (
|
||||
<button
|
||||
key={r.id}
|
||||
onClick={() => applyRecent(r)}
|
||||
className="text-xs px-2 py-1 rounded-full border border-border bg-muted hover:bg-muted/80 truncate max-w-[180px]"
|
||||
title={r.title}
|
||||
>
|
||||
{r.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-medium">Title *</label>
|
||||
<Input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="e.g. Certificate of Insurance Request" />
|
||||
</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)} placeholder="Additional details..." rows={2} className="text-sm" />
|
||||
</div>
|
||||
|
||||
{/* Client — only show search if not pre-filled */}
|
||||
{!context?.clientId && (
|
||||
<div className="space-y-1.5 relative">
|
||||
<label className="text-sm font-medium">Client *</label>
|
||||
<Input
|
||||
placeholder="Search client..."
|
||||
value={clientId ? clientSearch : clientSearch}
|
||||
onChange={(e) => { setClientSearch(e.target.value); setClientId(''); setClientDropOpen(true) }}
|
||||
readOnly={!!clientId}
|
||||
/>
|
||||
{clientId && (
|
||||
<button
|
||||
onClick={() => { setClientId(''); setClientSearch('') }}
|
||||
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) }}
|
||||
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' && context?.policies && context.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>
|
||||
{context.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' && context?.policyGroups && context.policyGroups.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>
|
||||
{context.policyGroups.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={d}>{d}</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 /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{claimsUsers.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>Claims</SelectLabel>
|
||||
{claimsUsers.map((u) => (
|
||||
<SelectItem key={u.id} value={u.id}>{u.displayName || u.email}</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
)}
|
||||
{otherUsers.length > 0 && (
|
||||
<SelectGroup>
|
||||
<SelectLabel>All Staff</SelectLabel>
|
||||
{otherUsers.map((u) => (
|
||||
<SelectItem key={u.id} value={u.id}>{u.displayName || u.email}</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>Cancel</Button>
|
||||
<Button onClick={handleSubmit} disabled={saving} className="gap-1.5">
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
{saving ? 'Creating...' : 'Create Task'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue