feat: sortable/filterable column header on task list

- Sort by Client, Date, Type (Group/Policy/Client), Level (priority)
- Click to sort asc/desc with arrow indicators
- Filter dropdowns for Type and Level with Clear button
- Applied after card filter and status tab filter
This commit is contained in:
lorentz 2026-04-08 21:23:46 +00:00
parent 33ec8b2799
commit d82b3dc611

View file

@ -21,7 +21,7 @@ import {
SelectValue,
} from '@/components/ui/select'
import { UserSelectContent } from '@/components/ui/user-select-content'
import { CheckSquare, Clock, AlertCircle, MessageSquare, CheckCircle2, RotateCcw, Eye, CalendarRange, ArrowRightLeft, Search, X, Building2, Plus, Ban } from 'lucide-react'
import { CheckSquare, Clock, AlertCircle, MessageSquare, CheckCircle2, RotateCcw, Eye, CalendarRange, ArrowRightLeft, Search, X, Building2, Plus, Ban, ArrowUpDown, ArrowUp, ArrowDown, Filter } from 'lucide-react'
import { Input } from '@/components/ui/input'
import { formatDate } from '@/lib/utils'
import { AdditionalServiceModal } from '@/components/tasks/additional-service-modal'
@ -450,6 +450,10 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users }
const [clientSearch, setClientSearch] = useState('')
const [clientOptions, setClientOptions] = useState<{id: string; name: string}[]>([])
const [clientSearchOpen, setClientSearchOpen] = useState(false)
const [sortKey, setSortKey] = useState<'client' | 'date' | 'type' | 'priority'>('date')
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc')
const [typeFilter, setTypeFilter] = useState<'all' | 'Group' | 'Policy' | 'Client'>('all')
const [priorityFilter, setPriorityFilter] = useState<'all' | 'HIGH' | 'MEDIUM' | 'LOW'>('all')
const viewingUser = users.find((u) => u.id === viewingUserId)
const isViewingSelf = viewingUserId === currentUserId
@ -552,11 +556,48 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users }
return base
}
const taskType = (t: Task): 'Group' | 'Policy' | 'Client' =>
t.policyGroup ? 'Group' : t.policy ? 'Policy' : 'Client'
const priorityOrder: Record<string, number> = { HIGH: 0, MEDIUM: 1, LOW: 2 }
const handleSort = (key: typeof sortKey) => {
if (sortKey === key) setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))
else { setSortKey(key); setSortDir('asc') }
}
const baseVisible =
filter === 'assigned' ? tasks.filter((t) => !isTerminal(t.status)) :
filter === 'completed' ? tasks.filter((t) => t.status === 'COMPLETED') :
tasks
const visibleTasks = applyCardFilter(baseVisible)
const afterCardFilter = applyCardFilter(baseVisible)
const afterColumnFilters = afterCardFilter.filter((t) => {
if (typeFilter !== 'all' && taskType(t) !== typeFilter) return false
if (priorityFilter !== 'all' && t.priority !== priorityFilter) return false
return true
})
const visibleTasks = [...afterColumnFilters].sort((a, b) => {
let cmp = 0
switch (sortKey) {
case 'client':
cmp = (a.client?.name ?? '').localeCompare(b.client?.name ?? '')
break
case 'date':
cmp = new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime()
break
case 'type': {
const typeOrder: Record<string, number> = { Group: 0, Policy: 1, Client: 2 }
cmp = (typeOrder[taskType(a)] ?? 3) - (typeOrder[taskType(b)] ?? 3)
break
}
case 'priority':
cmp = (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9)
break
}
return sortDir === 'asc' ? cmp : -cmp
})
const handleCardFilter = (f: typeof cardFilter) => {
setCardFilter((prev) => prev === f ? null : f)
@ -785,12 +826,86 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users }
</div>
</div>
</CardHeader>
{/* Column sort & filter header */}
<div className="px-6 pb-2">
<div className="flex items-center gap-1 flex-wrap rounded-md border border-border bg-muted/30 px-2 py-1.5 text-xs">
{/* Sort buttons */}
{([
['client', 'Client'],
['date', 'Date'],
['type', 'Type'],
['priority', 'Level'],
] as const).map(([key, label]) => (
<button
key={key}
onClick={() => handleSort(key)}
className={`flex items-center gap-1 px-2.5 py-1 rounded transition-colors font-medium ${
sortKey === key
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
}`}
>
{label}
{sortKey === key ? (
sortDir === 'asc' ? <ArrowUp className="h-3 w-3" /> : <ArrowDown className="h-3 w-3" />
) : (
<ArrowUpDown className="h-3 w-3 opacity-40" />
)}
</button>
))}
<span className="mx-1 h-4 w-px bg-border" />
{/* Type filter */}
<Select value={typeFilter} onValueChange={(v) => setTypeFilter(v as any)}>
<SelectTrigger className="h-7 w-[100px] text-xs border-0 bg-transparent shadow-none px-2">
<Filter className="h-3 w-3 mr-1 opacity-60" />
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="Group">Group</SelectItem>
<SelectItem value="Policy">Policy</SelectItem>
<SelectItem value="Client">Client</SelectItem>
</SelectContent>
</Select>
{/* Priority filter */}
<Select value={priorityFilter} onValueChange={(v) => setPriorityFilter(v as any)}>
<SelectTrigger className="h-7 w-[110px] text-xs border-0 bg-transparent shadow-none px-2">
<Filter className="h-3 w-3 mr-1 opacity-60" />
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Levels</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
</SelectContent>
</Select>
{(typeFilter !== 'all' || priorityFilter !== 'all') && (
<button
onClick={() => { setTypeFilter('all'); setPriorityFilter('all') }}
className="ml-1 flex items-center gap-1 px-2 py-1 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
>
<X className="h-3 w-3" /> Clear
</button>
)}
</div>
</div>
<CardContent>
{visibleTasks.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<CheckSquare className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p className="text-lg font-medium">No tasks assigned</p>
<p className="text-sm mt-2">Tasks will appear here when they are assigned to you</p>
<p className="text-lg font-medium">No tasks found</p>
<p className="text-sm mt-2">
{typeFilter !== 'all' || priorityFilter !== 'all'
? 'Try adjusting the filters above'
: 'Tasks will appear here when they are assigned to you'}
</p>
</div>
) : (
<div className="space-y-3">