feat(tasks): add per-client mode and preview-before-generate to Generate & Assign
This commit is contained in:
parent
adc3d9bfa7
commit
b8ea94e37b
1 changed files with 172 additions and 44 deletions
|
|
@ -22,12 +22,23 @@ import {
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/components/ui/alert-dialog'
|
||||||
import { Users, Filter, CheckSquare, Wand2, MessageSquare, Pencil } from 'lucide-react'
|
import { Users, Filter, CheckSquare, Wand2, MessageSquare, Pencil } from 'lucide-react'
|
||||||
import { formatDate, formatRenewalDate } from '@/lib/utils'
|
import { formatDate, formatRenewalDate } from '@/lib/utils'
|
||||||
import { TaskEditModal, type EditableTask } from '@/components/tasks/task-edit-modal'
|
import { TaskEditModal, type EditableTask } from '@/components/tasks/task-edit-modal'
|
||||||
|
|
||||||
interface SimpleUser { id: string; displayName: string | null; email: string; department: string | null }
|
interface SimpleUser { id: string; displayName: string | null; email: string; department: string | null }
|
||||||
interface SimpleClient { id: string; name: string }
|
interface SimpleClient { id: string; name: string; claimsAdvocateId: string | null }
|
||||||
interface SimpleDesignation { id: string; name: string }
|
interface SimpleDesignation { id: string; name: string }
|
||||||
|
|
||||||
interface BulkAssignClientProps {
|
interface BulkAssignClientProps {
|
||||||
|
|
@ -52,13 +63,20 @@ const DEPT_OPTIONS = [
|
||||||
{ value: 'OTHER', label: 'Other' },
|
{ value: 'OTHER', label: 'Other' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
interface ClientTaskSummary {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
estimatedTasks: number
|
||||||
|
}
|
||||||
|
|
||||||
interface GenResult {
|
interface GenResult {
|
||||||
|
dryRun: boolean
|
||||||
|
clientsFound: number
|
||||||
|
totalEstimatedTasks: number
|
||||||
|
advocateName: string | null
|
||||||
|
clients: ClientTaskSummary[]
|
||||||
tasksCreated: number
|
tasksCreated: number
|
||||||
tasksAssigned: number
|
tasksAssigned: number
|
||||||
clientsFound: number
|
|
||||||
groupsProcessed: number
|
|
||||||
advocateName: string | null
|
|
||||||
message?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BulkAssignClient({ users, clients, designations }: BulkAssignClientProps) {
|
export function BulkAssignClient({ users, clients, designations }: BulkAssignClientProps) {
|
||||||
|
|
@ -70,9 +88,13 @@ export function BulkAssignClient({ users, clients, designations }: BulkAssignCli
|
||||||
const [editingTask, setEditingTask] = useState<any | null>(null)
|
const [editingTask, setEditingTask] = useState<any | null>(null)
|
||||||
|
|
||||||
// Generate & assign state
|
// Generate & assign state
|
||||||
|
const [genMode, setGenMode] = useState<'advocate-designation' | 'client'>('advocate-designation')
|
||||||
const [genAdvocate, setGenAdvocate] = useState('')
|
const [genAdvocate, setGenAdvocate] = useState('')
|
||||||
const [genDesignation, setGenDesignation] = useState('')
|
const [genDesignation, setGenDesignation] = useState('')
|
||||||
|
const [genClientId, setGenClientId] = useState('')
|
||||||
|
const [previewing, setPreviewing] = useState(false)
|
||||||
const [generating, setGenerating] = useState(false)
|
const [generating, setGenerating] = useState(false)
|
||||||
|
const [preview, setPreview] = useState<GenResult | null>(null)
|
||||||
const [genResult, setGenResult] = useState<GenResult | null>(null)
|
const [genResult, setGenResult] = useState<GenResult | null>(null)
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
|
|
@ -140,26 +162,55 @@ export function BulkAssignClient({ users, clients, designations }: BulkAssignCli
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGenerateAndAssign = async () => {
|
const buildGenBody = (dryRun: boolean) =>
|
||||||
if (!genAdvocate || !genDesignation) return
|
genMode === 'client'
|
||||||
setGenerating(true)
|
? { clientId: genClientId, dryRun }
|
||||||
|
: { advocateId: genAdvocate, designationId: genDesignation, dryRun }
|
||||||
|
|
||||||
|
const handlePreview = async () => {
|
||||||
|
setPreviewing(true)
|
||||||
setGenResult(null)
|
setGenResult(null)
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/tasks/generate-and-assign', {
|
const res = await fetch('/api/tasks/generate-and-assign', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ advocateId: genAdvocate, designationId: genDesignation }),
|
body: JSON.stringify(buildGenBody(true)),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) throw new Error(data.error)
|
||||||
|
if (data.totalEstimatedTasks === 0) {
|
||||||
|
toast.info(
|
||||||
|
data.clientsFound === 0
|
||||||
|
? 'No clients found matching this selection'
|
||||||
|
: 'No new tasks to generate — all tasks may already exist'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setPreview(data)
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error(err.message || 'Failed to preview task generation')
|
||||||
|
} finally {
|
||||||
|
setPreviewing(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirmGenerate = async () => {
|
||||||
|
setGenerating(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/tasks/generate-and-assign', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(buildGenBody(false)),
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (!res.ok) throw new Error(data.error)
|
if (!res.ok) throw new Error(data.error)
|
||||||
setGenResult(data)
|
setGenResult(data)
|
||||||
|
setPreview(null)
|
||||||
if (data.tasksCreated > 0) {
|
if (data.tasksCreated > 0) {
|
||||||
toast.success(`Generated ${data.tasksCreated} task(s) and assigned to ${data.advocateName}`)
|
toast.success(`Generated ${data.tasksCreated} task(s) and assigned to ${data.advocateName}`)
|
||||||
fetchTasks()
|
fetchTasks()
|
||||||
} else if (data.clientsFound === 0) {
|
|
||||||
toast.info('No clients found with this advocate and designation')
|
|
||||||
} else {
|
} else {
|
||||||
toast.info(data.message || 'No new tasks to generate')
|
toast.info('No new tasks to generate')
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.error(err.message || 'Failed to generate tasks')
|
toast.error(err.message || 'Failed to generate tasks')
|
||||||
|
|
@ -194,49 +245,126 @@ export function BulkAssignClient({ users, clients, designations }: BulkAssignCli
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex items-end gap-3 flex-wrap">
|
<Tabs
|
||||||
<div className="flex-1 min-w-[200px]">
|
value={genMode}
|
||||||
<p className="text-sm font-medium mb-1.5">Claims Advocate</p>
|
onValueChange={(v) => { setGenMode(v as 'advocate-designation' | 'client'); setGenResult(null) }}
|
||||||
<Select value={genAdvocate || '_none'} onValueChange={(v) => { setGenAdvocate(v === '_none' ? '' : v); setGenResult(null) }}>
|
>
|
||||||
<SelectTrigger>
|
<TabsList>
|
||||||
<SelectValue placeholder="Select advocate..." />
|
<TabsTrigger value="advocate-designation">By Advocate + Designation</TabsTrigger>
|
||||||
</SelectTrigger>
|
<TabsTrigger value="client">By Client</TabsTrigger>
|
||||||
<UserSelectContent
|
</TabsList>
|
||||||
users={users}
|
</Tabs>
|
||||||
prefixItem={<SelectItem value="_none">Select advocate</SelectItem>}
|
|
||||||
/>
|
{genMode === 'advocate-designation' ? (
|
||||||
</Select>
|
<div className="flex items-end gap-3 flex-wrap mt-3">
|
||||||
|
<div className="flex-1 min-w-[200px]">
|
||||||
|
<p className="text-sm font-medium mb-1.5">Claims Advocate</p>
|
||||||
|
<Select value={genAdvocate || '_none'} onValueChange={(v) => { setGenAdvocate(v === '_none' ? '' : v); setGenResult(null) }}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select advocate..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<UserSelectContent
|
||||||
|
users={users}
|
||||||
|
prefixItem={<SelectItem value="_none">Select advocate</SelectItem>}
|
||||||
|
/>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-[200px]">
|
||||||
|
<p className="text-sm font-medium mb-1.5">Designation</p>
|
||||||
|
<Select value={genDesignation || '_none'} onValueChange={(v) => { setGenDesignation(v === '_none' ? '' : v); setGenResult(null) }}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select designation..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="_none">Select designation</SelectItem>
|
||||||
|
{designations.map((d) => (
|
||||||
|
<SelectItem key={d.id} value={d.id}>{d.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handlePreview} disabled={!genAdvocate || !genDesignation || previewing}>
|
||||||
|
{previewing ? 'Checking...' : 'Preview'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-[200px]">
|
) : (
|
||||||
<p className="text-sm font-medium mb-1.5">Designation</p>
|
<div className="flex items-end gap-3 flex-wrap mt-3">
|
||||||
<Select value={genDesignation || '_none'} onValueChange={(v) => { setGenDesignation(v === '_none' ? '' : v); setGenResult(null) }}>
|
<div className="flex-1 min-w-[240px]">
|
||||||
<SelectTrigger>
|
<p className="text-sm font-medium mb-1.5">Client</p>
|
||||||
<SelectValue placeholder="Select designation..." />
|
<Select value={genClientId || '_none'} onValueChange={(v) => { setGenClientId(v === '_none' ? '' : v); setGenResult(null) }}>
|
||||||
</SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectContent>
|
<SelectValue placeholder="Select client..." />
|
||||||
<SelectItem value="_none">Select designation</SelectItem>
|
</SelectTrigger>
|
||||||
{designations.map((d) => (
|
<SelectContent>
|
||||||
<SelectItem key={d.id} value={d.id}>{d.name}</SelectItem>
|
<SelectItem value="_none">Select client</SelectItem>
|
||||||
))}
|
{clients.map((c) => (
|
||||||
</SelectContent>
|
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
|
||||||
</Select>
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{genClientId && !clients.find((c) => c.id === genClientId)?.claimsAdvocateId && (
|
||||||
|
<p className="text-sm text-destructive mt-1.5">
|
||||||
|
This client has no claims advocate assigned. Set one before generating tasks.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handlePreview}
|
||||||
|
disabled={!genClientId || !clients.find((c) => c.id === genClientId)?.claimsAdvocateId || previewing}
|
||||||
|
>
|
||||||
|
{previewing ? 'Checking...' : 'Preview'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleGenerateAndAssign} disabled={!genAdvocate || !genDesignation || generating}>
|
)}
|
||||||
{generating ? 'Generating...' : 'Generate & Assign'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{genResult && (
|
{genResult && (
|
||||||
<p className="mt-3 text-sm text-muted-foreground">
|
<p className="mt-3 text-sm text-muted-foreground">
|
||||||
{genResult.tasksCreated > 0
|
{genResult.tasksCreated > 0
|
||||||
? `Created ${genResult.tasksCreated} task(s) across ${genResult.clientsFound} client(s) and assigned to ${genResult.advocateName}.`
|
? `Created ${genResult.tasksCreated} task(s) across ${genResult.clientsFound} client(s) and assigned to ${genResult.advocateName}.`
|
||||||
: genResult.clientsFound === 0
|
: 'No new tasks to generate — all tasks may already exist.'}
|
||||||
? `No clients found assigned to this advocate with that designation.`
|
|
||||||
: genResult.message || 'No new tasks to generate — all tasks may already exist.'}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<AlertDialog open={!!preview} onOpenChange={(open) => { if (!open) setPreview(null) }}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Generate {preview?.totalEstimatedTasks} task{preview && preview.totalEstimatedTasks !== 1 ? 's' : ''}?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will create {preview?.totalEstimatedTasks} task(s) across {preview?.clientsFound} client(s)
|
||||||
|
and assign them to {preview?.advocateName}.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<div className="max-h-64 overflow-y-auto rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Client</TableHead>
|
||||||
|
<TableHead className="text-right">Tasks</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{preview?.clients.map((c) => (
|
||||||
|
<TableRow key={c.id}>
|
||||||
|
<TableCell>{c.name}</TableCell>
|
||||||
|
<TableCell className="text-right">{c.estimatedTasks}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleConfirmGenerate} disabled={generating}>
|
||||||
|
{generating ? 'Generating...' : 'Confirm'}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
|
||||||
{/* Filter bar */}
|
{/* Filter bar */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue