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:
parent
17f475cd9c
commit
170bb35921
1 changed files with 125 additions and 30 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue