Tasks: client filter search, unified fetchTasks, fix transfer refresh

This commit is contained in:
lorentz 2026-03-31 16:09:33 +00:00
parent 881b82f986
commit c5c3d5a4ab

View file

@ -1,6 +1,6 @@
'use client'
import { useState, useCallback } from 'react'
import { useState, useCallback, useEffect } from 'react'
import { toast } from 'sonner'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
@ -22,7 +22,8 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { CheckSquare, Clock, AlertCircle, MessageSquare, CheckCircle2, RotateCcw, Eye, CalendarRange, ArrowRightLeft } from 'lucide-react'
import { CheckSquare, Clock, AlertCircle, MessageSquare, CheckCircle2, RotateCcw, Eye, CalendarRange, ArrowRightLeft, Search, X, Building2 } from 'lucide-react'
import { Input } from '@/components/ui/input'
import { formatDate } from '@/lib/utils'
interface TaskUser { displayName: string | null; email: string }
@ -291,14 +292,21 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users }
const [transferring, setTransferring] = useState(false)
const [viewingUserId, setViewingUserId] = useState(currentUserId)
const [loadingTasks, setLoadingTasks] = useState(false)
const [clientFilter, setClientFilter] = useState('')
const [clientSearch, setClientSearch] = useState('')
const [clientOptions, setClientOptions] = useState<{id: string; name: string}[]>([])
const [clientSearchOpen, setClientSearchOpen] = useState(false)
const viewingUser = users.find((u) => u.id === viewingUserId)
const isViewingSelf = viewingUserId === currentUserId
const selectedClient = clientOptions.find((c) => c.id === clientFilter)
const fetchForUser = useCallback(async (uid: string) => {
const fetchTasks = useCallback(async (uid: string, cid?: string) => {
setLoadingTasks(true)
try {
const params = new URLSearchParams({ userId: uid, limit: '100' })
const params = new URLSearchParams({ limit: '200' })
params.set('userId', uid)
if (cid) params.set('clientId', cid)
const res = await fetch(`/api/tasks?${params}`)
const data = await res.json()
if (!res.ok) throw new Error(data.error)
@ -312,13 +320,28 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users }
const handleUserChange = (uid: string) => {
setViewingUserId(uid)
if (uid === currentUserId) {
setTasks(initialTasks)
} else {
fetchForUser(uid)
}
fetchTasks(uid, clientFilter || undefined)
}
const handleClientFilterChange = (cid: string) => {
setClientFilter(cid)
fetchTasks(viewingUserId, cid || undefined)
setClientSearchOpen(false)
setClientSearch('')
}
useEffect(() => {
if (!clientSearch.trim()) { setClientOptions([]); return }
const t = setTimeout(async () => {
try {
const res = await fetch(`/api/clients?search=${encodeURIComponent(clientSearch)}&limit=20`)
const data = await res.json()
setClientOptions((data.clients || []).map((c: any) => ({ id: c.id, name: c.name })))
} catch {}
}, 250)
return () => clearTimeout(t)
}, [clientSearch])
const handleTransfer = async () => {
if (!transferTo || !visibleTasks.length) return
setTransferring(true)
@ -341,7 +364,7 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users }
if (viewingUserId === currentUserId) {
setTasks((prev) => prev.filter((t) => !visibleTasks.find((v) => v.id === t.id)))
} else {
fetchForUser(viewingUserId)
fetchTasks(viewingUserId, clientFilter || undefined)
}
} catch (err: any) {
toast.error(err.message || 'Transfer failed')
@ -437,6 +460,47 @@ export function TasksClient({ initialTasks, currentUserId, isPrivileged, users }
)}
</div>
{/* Client filter */}
<div className="relative">
<div className="relative">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Filter by client..."
value={clientFilter ? (selectedClient?.name ?? '') : clientSearch}
onChange={(e) => {
setClientSearch(e.target.value)
if (!e.target.value) { setClientFilter(''); fetchTasks(viewingUserId) }
setClientSearchOpen(true)
}}
onFocus={() => { if (!clientFilter) setClientSearchOpen(true) }}
className="pl-9 pr-8"
readOnly={!!clientFilter}
/>
{clientFilter && (
<button
onClick={() => { setClientFilter(''); setClientSearch(''); fetchTasks(viewingUserId) }}
className="absolute right-2 top-2.5 text-muted-foreground hover:text-foreground"
>
<X className="h-4 w-4" />
</button>
)}
</div>
{clientSearchOpen && clientOptions.length > 0 && !clientFilter && (
<div className="absolute z-50 mt-1 w-full rounded-md border border-border bg-popover shadow-md">
{clientOptions.map((c) => (
<button
key={c.id}
onClick={() => handleClientFilterChange(c.id)}
className="flex items-center gap-2 w-full px-3 py-2 text-sm hover:bg-muted text-left"
>
<Building2 className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
{c.name}
</button>
))}
</div>
)}
</div>
{/* Stats / filter cards */}
<div className="grid gap-3 grid-cols-2 lg:grid-cols-5">
{/* Overdue */}