seubert-claims/ondeck/src/components/admin/task-template-manager.tsx

746 lines
28 KiB
TypeScript

'use client'
import { useState } from 'react'
import { toast } from 'sonner'
import {
Plus,
Search,
Pencil,
Trash2,
Clock,
Calendar,
Building2,
Tag,
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Label } from '@/components/ui/label'
type DepartmentType = 'PERSONAL_LINES' | 'COMMERCIAL_LINES' | 'CLAIMS' | 'BENEFITS' | 'OTHER'
type TaskTiming = 'PRE_RENEWAL' | 'POST_RENEWAL'
type TaskPriority = 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT'
type TaskLevel = 'POLICY' | 'RENEWAL_GROUP' | 'BOTH' | 'CLIENT'
interface Designation {
id: string
name: string
color: string
}
interface TaskTemplate {
id: string
name: string
description: string | null
department: DepartmentType
timing: TaskTiming
daysOffset: number
defaultPriority: TaskPriority
taskGroup: string | null
isActive: boolean
displayOrder: number | null
level: TaskLevel
designationId: string | null
policyTypeFilter: string | null
designation: Designation | null
_count: { tasks: number }
}
interface TaskTemplateManagerProps {
initialTemplates: TaskTemplate[]
designations: Designation[]
}
const DEPARTMENTS: { value: DepartmentType; label: string }[] = [
{ value: 'PERSONAL_LINES', label: 'Personal Lines' },
{ value: 'COMMERCIAL_LINES', label: 'Commercial Lines' },
{ value: 'CLAIMS', label: 'Claims' },
{ value: 'BENEFITS', label: 'Benefits' },
{ value: 'OTHER', label: 'Other' },
]
const TIMINGS: { value: TaskTiming; label: string }[] = [
{ value: 'PRE_RENEWAL', label: 'Pre-Renewal' },
{ value: 'POST_RENEWAL', label: 'Post-Renewal' },
]
const PRIORITIES: { value: TaskPriority; label: string; color: string }[] = [
{ value: 'LOW', label: 'Low', color: 'bg-slate-100 text-slate-700' },
{ value: 'MEDIUM', label: 'Medium', color: 'bg-blue-100 text-blue-700' },
{ value: 'HIGH', label: 'High', color: 'bg-orange-100 text-orange-700' },
{ value: 'URGENT', label: 'Urgent', color: 'bg-red-100 text-red-700' },
]
const LEVELS: { value: TaskLevel; label: string }[] = [
{ value: 'CLIENT', label: 'Client' },
{ value: 'BOTH', label: 'Policy/Group' },
{ value: 'RENEWAL_GROUP', label: 'Group only' },
{ value: 'POLICY', label: 'Policy only' },
]
const POLICY_TYPES = [
'Workers Compensation',
'Package',
'General Liability',
'Business Auto',
'Umbrella(C)',
'Commercial Property',
'Inland Marine (C)',
'Cyber Liability',
'BOP',
'Crime',
'Pollution Liability',
'Errors & Ommissions Professional Liability',
'Employment Practices Liability',
'Directors & Officers',
'Motor Truck Cargo',
'ERISA',
]
const emptyTemplate = {
name: '',
description: '',
department: 'CLAIMS' as DepartmentType,
timing: 'PRE_RENEWAL' as TaskTiming,
daysOffset: -90,
defaultPriority: 'MEDIUM' as TaskPriority,
taskGroup: '',
isActive: true,
displayOrder: null as number | null,
level: 'BOTH' as TaskLevel,
designationId: null as string | null,
policyTypeFilter: null as string | null,
}
export function TaskTemplateManager({
initialTemplates,
designations,
}: TaskTemplateManagerProps) {
const [templates, setTemplates] = useState<TaskTemplate[]>(initialTemplates)
const [searchQuery, setSearchQuery] = useState('')
const [departmentFilter, setDepartmentFilter] = useState<string>('all')
const [timingFilter, setTimingFilter] = useState<string>('all')
const [designationFilter, setDesignationFilter] = useState<string>('all')
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [editingTemplate, setEditingTemplate] = useState<TaskTemplate | null>(null)
const [formData, setFormData] = useState(emptyTemplate)
const [isSubmitting, setIsSubmitting] = useState(false)
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null)
const filteredTemplates = templates.filter((template) => {
const matchesSearch =
template.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
template.description?.toLowerCase().includes(searchQuery.toLowerCase())
const matchesDepartment =
departmentFilter === 'all' || template.department === departmentFilter
const matchesTiming =
timingFilter === 'all' || template.timing === timingFilter
const matchesDesignation =
designationFilter === 'all' ||
(designationFilter === 'none' && !template.designationId) ||
template.designationId === designationFilter
return matchesSearch && matchesDepartment && matchesTiming && matchesDesignation
})
const handleOpenCreate = () => {
setEditingTemplate(null)
setFormData(emptyTemplate)
setIsDialogOpen(true)
}
const handleOpenEdit = (template: TaskTemplate) => {
setEditingTemplate(template)
setFormData({
name: template.name,
description: template.description || '',
department: template.department,
timing: template.timing,
daysOffset: template.daysOffset,
defaultPriority: template.defaultPriority,
taskGroup: template.taskGroup || '',
isActive: template.isActive,
displayOrder: template.displayOrder,
level: template.level,
designationId: template.designationId,
policyTypeFilter: template.policyTypeFilter,
})
setIsDialogOpen(true)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
try {
const url = editingTemplate
? `/api/templates/${editingTemplate.id}`
: '/api/templates'
const method = editingTemplate ? 'PATCH' : 'POST'
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...formData,
description: formData.description || null,
displayOrder: formData.displayOrder || null,
designationId: formData.designationId || null,
policyTypeFilter: formData.policyTypeFilter || null,
}),
})
if (!response.ok) {
throw new Error('Failed to save template')
}
const savedTemplate = await response.json()
if (editingTemplate) {
setTemplates((prev) =>
prev.map((t) =>
t.id === savedTemplate.id
? { ...savedTemplate, _count: t._count, designation: designations.find(d => d.id === savedTemplate.designationId) || null }
: t
)
)
toast.success('Template updated successfully')
} else {
setTemplates((prev) => [
...prev,
{ ...savedTemplate, _count: { tasks: 0 }, designation: designations.find(d => d.id === savedTemplate.designationId) || null },
])
toast.success('Template created successfully')
}
setIsDialogOpen(false)
} catch (error) {
toast.error('Failed to save template')
} finally {
setIsSubmitting(false)
}
}
const handleDelete = async (id: string) => {
try {
const response = await fetch(`/api/templates/${id}`, {
method: 'DELETE',
})
if (!response.ok) {
throw new Error('Failed to delete template')
}
setTemplates((prev) => prev.filter((t) => t.id !== id))
toast.success('Template deleted successfully')
} catch (error) {
toast.error('Failed to delete template')
} finally {
setDeleteConfirmId(null)
}
}
const formatDaysOffset = (timing: TaskTiming, daysOffset: number) => {
const absDays = Math.abs(daysOffset)
if (timing === 'PRE_RENEWAL') {
return `${absDays} days before renewal`
}
return `${absDays} days after renewal`
}
const getPriorityBadge = (priority: TaskPriority) => {
const config = PRIORITIES.find((p) => p.value === priority)
return (
<Badge variant="secondary" className={config?.color}>
{config?.label}
</Badge>
)
}
return (
<div className="space-y-6">
{/* Filters and Actions */}
<Card>
<CardContent className="pt-6">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex flex-1 flex-col gap-4 md:flex-row md:items-center">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Search templates..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<Select value={departmentFilter} onValueChange={setDepartmentFilter}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Department" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Departments</SelectItem>
{DEPARTMENTS.map((dept) => (
<SelectItem key={dept.value} value={dept.value}>
{dept.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={timingFilter} onValueChange={setTimingFilter}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="Timing" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Timing</SelectItem>
{TIMINGS.map((timing) => (
<SelectItem key={timing.value} value={timing.value}>
{timing.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={designationFilter} onValueChange={setDesignationFilter}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Designation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Designations</SelectItem>
<SelectItem value="none">No Designation</SelectItem>
{designations.map((des) => (
<SelectItem key={des.id} value={des.id}>
{des.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button onClick={handleOpenCreate}>
<Plus className="mr-2 h-4 w-4" />
Add Template
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[500px]">
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>
{editingTemplate ? 'Edit Template' : 'Create Template'}
</DialogTitle>
<DialogDescription>
{editingTemplate
? 'Update the task template details.'
: 'Create a new task template for renewal workflows.'}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={formData.name}
onChange={(e) =>
setFormData((prev) => ({ ...prev, name: e.target.value }))
}
placeholder="e.g., Claim Review"
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={formData.description}
onChange={(e) =>
setFormData((prev) => ({
...prev,
description: e.target.value,
}))
}
placeholder="Optional description"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="taskGroup">Task Group</Label>
<Input
id="taskGroup"
value={formData.taskGroup}
onChange={(e) =>
setFormData((prev) => ({ ...prev, taskGroup: e.target.value }))
}
placeholder="e.g. Renewal, Claims, Audit (optional)"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label>Department</Label>
<Select
value={formData.department}
onValueChange={(value: DepartmentType) =>
setFormData((prev) => ({ ...prev, department: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{DEPARTMENTS.map((dept) => (
<SelectItem key={dept.value} value={dept.value}>
{dept.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label>Timing</Label>
<Select
value={formData.timing}
onValueChange={(value: TaskTiming) =>
setFormData((prev) => ({ ...prev, timing: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{TIMINGS.map((timing) => (
<SelectItem key={timing.value} value={timing.value}>
{timing.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="daysOffset">Days Offset</Label>
<Input
id="daysOffset"
type="number"
value={formData.daysOffset}
onChange={(e) =>
setFormData((prev) => ({
...prev,
daysOffset: parseInt(e.target.value) || 0,
}))
}
/>
<p className="text-xs text-muted-foreground">
Negative for pre-renewal, positive for post-renewal
</p>
</div>
<div className="grid gap-2">
<Label>Priority</Label>
<Select
value={formData.defaultPriority}
onValueChange={(value: TaskPriority) =>
setFormData((prev) => ({
...prev,
defaultPriority: value,
}))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{PRIORITIES.map((priority) => (
<SelectItem key={priority.value} value={priority.value}>
{priority.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label>Level</Label>
<Select
value={formData.level}
onValueChange={(value: TaskLevel) =>
setFormData((prev) => ({ ...prev, level: value }))
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{LEVELS.map((lvl) => (
<SelectItem key={lvl.value} value={lvl.value}>
{lvl.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label>Policy Type Filter</Label>
<Select
value={formData.policyTypeFilter || 'none'}
onValueChange={(value) =>
setFormData((prev) => ({
...prev,
policyTypeFilter: value === 'none' ? null : value,
}))
}
>
<SelectTrigger>
<SelectValue placeholder="Any policy type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">Any policy type</SelectItem>
{POLICY_TYPES.map((pt) => (
<SelectItem key={pt} value={pt}>
{pt}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">Only generate this task for policies of this type. Leave blank for all types.</p>
</div>
<div className="grid gap-2">
<Label>Designation</Label>
<Select
value={formData.designationId || 'none'}
onValueChange={(value) =>
setFormData((prev) => ({
...prev,
designationId: value === 'none' ? null : value,
}))
}
>
<SelectTrigger>
<SelectValue placeholder="Select designation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None (All clients)</SelectItem>
{designations.map((des) => (
<SelectItem key={des.id} value={des.id}>
{des.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="displayOrder">Display Order</Label>
<Input
id="displayOrder"
type="number"
value={formData.displayOrder ?? ''}
onChange={(e) =>
setFormData((prev) => ({
...prev,
displayOrder: e.target.value
? parseInt(e.target.value)
: null,
}))
}
placeholder="Optional"
/>
</div>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="isActive"
checked={formData.isActive}
onChange={(e) =>
setFormData((prev) => ({
...prev,
isActive: e.target.checked,
}))
}
className="h-4 w-4 rounded border-gray-300"
/>
<Label htmlFor="isActive">Active</Label>
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setIsDialogOpen(false)}
>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting
? 'Saving...'
: editingTemplate
? 'Update'
: 'Create'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</CardContent>
</Card>
{/* Templates Table */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Templates ({filteredTemplates.length})</span>
</CardTitle>
</CardHeader>
<CardContent>
{filteredTemplates.length === 0 ? (
<div className="py-8 text-center text-muted-foreground">
No templates found. Create one to get started.
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Department</TableHead>
<TableHead>Timing</TableHead>
<TableHead>Level</TableHead>
<TableHead>Policy Type</TableHead>
<TableHead>Designation</TableHead>
<TableHead>Priority</TableHead>
<TableHead>Status</TableHead>
<TableHead>Tasks</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredTemplates.map((template) => (
<TableRow key={template.id}>
<TableCell>
<div>
<div className="font-medium">{template.name}</div>
{template.description && (
<div className="text-sm text-muted-foreground">
{template.description}
</div>
)}
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Building2 className="h-3 w-3 text-muted-foreground" />
<span className="text-sm">
{DEPARTMENTS.find((d) => d.value === template.department)
?.label || template.department}
</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Clock className="h-3 w-3 text-muted-foreground" />
<span className="text-sm">
{formatDaysOffset(template.timing, template.daysOffset)}
</span>
</div>
</TableCell>
<TableCell>
<span className="text-sm">
{LEVELS.find((l) => l.value === template.level)?.label || template.level}
</span>
</TableCell>
<TableCell>
{template.policyTypeFilter ? (
<Badge variant="outline" className="text-xs font-normal">{template.policyTypeFilter}</Badge>
) : (
<span className="text-xs text-muted-foreground">Any</span>
)}
</TableCell>
<TableCell>
{template.designation ? (
<Badge variant="outline" className="flex items-center gap-1 w-fit">
<Tag className="h-3 w-3" />
{template.designation.name}
</Badge>
) : (
<span className="text-sm text-muted-foreground">All</span>
)}
</TableCell>
<TableCell>{getPriorityBadge(template.defaultPriority)}</TableCell>
<TableCell>
<Badge variant={template.isActive ? 'default' : 'secondary'}>
{template.isActive ? 'Active' : 'Inactive'}
</Badge>
</TableCell>
<TableCell>
<span className="text-sm text-muted-foreground">
{template._count.tasks}
</span>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleOpenEdit(template)}
>
<Pencil className="h-4 w-4" />
</Button>
{deleteConfirmId === template.id ? (
<div className="flex items-center gap-1">
<Button
variant="destructive"
size="sm"
onClick={() => handleDelete(template.id)}
>
Confirm
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setDeleteConfirmId(null)}
>
Cancel
</Button>
</div>
) : (
<Button
variant="ghost"
size="sm"
onClick={() => setDeleteConfirmId(template.id)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
)
}