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:
parent
33ec8b2799
commit
d82b3dc611
1 changed files with 119 additions and 4 deletions
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue