feat: add sort/filter header to client detail tasks tab

- Sort by Date or Priority (asc/desc toggle)
- Hide completed/NA/cancelled tasks by default
- Show Completed toggle button
- Show Archived button for old tasks
- Richer task cards with status badges, priority pills, overdue highlighting
- Tab count shows active tasks only
This commit is contained in:
lorentz 2026-04-09 01:05:29 +00:00
parent 17f475cd9c
commit 170bb35921

View file

@ -14,7 +14,7 @@ import {
SelectValue,
} from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { Building2, MapPin, Phone, Mail, FileText, CheckSquare, CalendarRange, Users, X, Plus, UserCheck, StickyNote, Pencil, Trash2, ChevronDown, ChevronUp } from 'lucide-react'
import { Building2, MapPin, Phone, Mail, FileText, CheckSquare, CalendarRange, Users, X, Plus, UserCheck, StickyNote, Pencil, Trash2, ChevronDown, ChevronUp, ArrowUp, ArrowDown, ArrowUpDown } from 'lucide-react'
import { Combobox } from '@/components/ui/combobox'
import { formatDate, formatRenewalDate } from '@/lib/utils'
import { PolicyGroupManager } from '@/components/clients/policy-group-manager'
@ -53,6 +53,9 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
const [parentOptions, setParentOptions] = useState<{id:string;name:string}[]>([])
const [parentLinking, setParentLinking] = useState(false)
const [showArchivedTasks, setShowArchivedTasks] = useState(false)
const [taskSort, setTaskSort] = useState<'date' | 'priority'>('date')
const [taskSortDir, setTaskSortDir] = useState<'asc' | 'desc'>('asc')
const [showCompleted, setShowCompleted] = useState(false)
const [notesExpanded, setNotesExpanded] = useState(!!(client.notes))
const [additionalServiceOpen, setAdditionalServiceOpen] = useState(false)
const [sessionUserId, setSessionUserId] = useState('')
@ -362,7 +365,7 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
</TabsTrigger>
<TabsTrigger value="tasks">
<CheckSquare className="h-4 w-4 mr-2" />
Tasks ({tasks.length})
Tasks ({tasks.filter((t: any) => t.status !== 'COMPLETED' && t.status !== 'NA' && t.status !== 'CANCELLED').length})
</TabsTrigger>
<TabsTrigger value="renewal-groups">
<CalendarRange className="h-4 w-4 mr-2" />
@ -475,44 +478,136 @@ export function ClientDetail({ client, designations, policyGroups = [], allPolic
</TabsContent>
<TabsContent value="tasks" className="space-y-4">
<div className="flex justify-between items-center">
{/* Sort / filter header */}
<div className="flex items-center gap-3 flex-wrap rounded-md border border-border bg-muted/30 px-4 py-2.5 text-sm">
{/* Sort buttons */}
{([['date', 'Date'], ['priority', 'Priority']] as const).map(([key, label]) => (
<button
key={key}
onClick={() => {
if (taskSort === key) setTaskSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))
else { setTaskSort(key); setTaskSortDir('asc') }
}}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded transition-colors font-medium ${
taskSort === key
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
}`}
>
{label}
{taskSort === key ? (
taskSortDir === 'asc' ? <ArrowUp className="h-3.5 w-3.5" /> : <ArrowDown className="h-3.5 w-3.5" />
) : (
<ArrowUpDown className="h-3.5 w-3.5 opacity-40" />
)}
</button>
))}
<span className="flex-1" />
{/* Show completed toggle */}
<button
onClick={() => setShowCompleted((v) => !v)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded transition-colors font-medium ${
showCompleted
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
}`}
>
<CheckSquare className="h-3.5 w-3.5" />
{showCompleted ? 'Hide Completed' : 'Show Completed'}
</button>
<span className="mx-1 h-5 w-px bg-border" />
<Button
size="sm"
variant={showArchivedTasks ? 'default' : 'outline'}
className="h-8 text-sm"
onClick={handleToggleArchived}
>
{showArchivedTasks ? 'Hide archived' : 'Show archived'}
{showArchivedTasks ? 'Hide Archived' : 'Show Archived'}
</Button>
<Button size="sm" className="gap-1.5" onClick={() => setAdditionalServiceOpen(true)}>
<Plus className="h-4 w-4" /> Additional Service
<Button size="sm" className="gap-1.5 h-8 text-sm" onClick={() => setAdditionalServiceOpen(true)}>
<Plus className="h-3.5 w-3.5" /> Additional Service
</Button>
</div>
{tasks.length === 0 ? (
<Card>
<CardContent className="pt-6 text-center text-muted-foreground">
No tasks found
</CardContent>
</Card>
) : (
tasks.map((task: any) => (
<Card key={task.id}>
<CardContent className="pt-6">
<div className="flex justify-between items-start">
<div>
<h3 className="font-semibold">{task.title}</h3>
<p className="text-sm text-muted-foreground">{task.description}</p>
</div>
<div className="text-right">
<Badge>{task.status}</Badge>
<p className="text-sm text-muted-foreground mt-1" suppressHydrationWarning>
Due: {formatDate(task.dueDate)}
</p>
</div>
</div>
{(() => {
const isTerminal = (s: string) => s === 'COMPLETED' || s === 'NA' || s === 'CANCELLED'
const priorityOrder: Record<string, number> = { HIGH: 0, MEDIUM: 1, LOW: 2 }
const filtered = showCompleted ? tasks : tasks.filter((t: any) => !isTerminal(t.status))
const sorted = [...filtered].sort((a: any, b: any) => {
let cmp = 0
if (taskSort === 'date') {
cmp = new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime()
} else {
cmp = (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9)
}
return taskSortDir === 'asc' ? cmp : -cmp
})
return sorted.length === 0 ? (
<Card>
<CardContent className="pt-6 text-center text-muted-foreground">
No tasks found
</CardContent>
</Card>
))
)}
) : (
sorted.map((task: any) => {
const completed = isTerminal(task.status)
const overdue = new Date(task.dueDate) < new Date() && !completed
return (
<Card
key={task.id}
className={completed ? 'opacity-60' : overdue ? 'border-red-500/40 bg-red-500/5' : ''}
>
<CardContent className="pt-6">
<div className="flex justify-between items-start">
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 mb-1">
<h3 className={`font-semibold ${completed ? 'line-through text-muted-foreground' : ''}`}>
{task.title}
</h3>
<Badge
variant={
task.status === 'COMPLETED' ? 'default' :
task.status === 'IN_PROGRESS' ? 'secondary' :
task.status === 'NA' || task.status === 'CANCELLED' ? 'outline' :
'secondary'
}
>
{task.status.replace('_', ' ')}
</Badge>
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
task.priority === 'HIGH' ? 'bg-red-500/15 text-red-700 dark:text-red-400' :
task.priority === 'MEDIUM' ? 'bg-yellow-500/15 text-yellow-700 dark:text-yellow-400' :
'bg-muted text-muted-foreground'
}`}>
{task.priority}
</span>
</div>
{task.description && (
<p className="text-sm text-muted-foreground line-clamp-2">{task.description}</p>
)}
{task.assignments?.length > 0 && (
<p className="mt-1 text-sm text-muted-foreground">
Assigned to: {task.assignments.map((a: any) => a.user.displayName || a.user.email).join(', ')}
</p>
)}
</div>
<div className="text-right shrink-0 ml-4">
<p className={`text-sm font-medium ${overdue ? 'text-red-500' : ''}`} suppressHydrationWarning>
Due: {formatDate(task.dueDate)}
</p>
</div>
</div>
</CardContent>
</Card>
)
})
)
})()}
</TabsContent>
<AdditionalServiceModal