feat(tasks): add per-client mode and preview-before-generate to Generate & Assign

This commit is contained in:
lorentz 2026-07-18 13:05:12 +00:00
parent adc3d9bfa7
commit b8ea94e37b

View file

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