seubert-claims/ondeck/src/components/clients/client-list.tsx

250 lines
8 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { Client, Designation } from '@prisma/client'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { ClientCard } from './client-card'
import { ClientTable } from './client-table'
import { Search, Filter, LayoutGrid, List, X, EyeOff, Eye } from 'lucide-react'
type ViewMode = 'cards' | 'table'
interface SimpleUser {
id: string
displayName: string | null
email: string
department?: string | null
}
interface ClientWithRelations extends Client {
designation: Designation | null
designation2: Designation | null
policies: any[]
_count: {
policies: number
tasks: number
}
}
interface ClientListProps {
initialClients?: ClientWithRelations[]
designations?: Designation[]
}
export function ClientList({ initialClients = [], designations = [] }: ClientListProps) {
const [clients, setClients] = useState<ClientWithRelations[]>(initialClients)
const [loading, setLoading] = useState(false)
const [search, setSearch] = useState('')
const [designationFilter, setDesignationFilter] = useState<string>('')
const [advocateFilter, setAdvocateFilter] = useState<string>('')
const [teamMemberFilter, setTeamMemberFilter] = useState<string>('')
const [page, setPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const [viewMode, setViewMode] = useState<ViewMode>('cards')
const [showNoPolicies, setShowNoPolicies] = useState(false)
const [users, setUsers] = useState<SimpleUser[]>([])
const fetchClients = async () => {
setLoading(true)
try {
const params = new URLSearchParams({
page: page.toString(),
limit: '20',
...(search && { search }),
...(designationFilter && { designationId: designationFilter }),
...(advocateFilter && { advocateId: advocateFilter }),
...(teamMemberFilter && { teamMemberId: teamMemberFilter }),
...(!showNoPolicies && { hasPolicies: 'true' }),
})
const response = await fetch(`/api/clients?${params}`)
const data = await response.json()
if (!response.ok) {
console.error('Clients API error:', data.error, data.detail)
return
}
setClients(data.clients ?? [])
setTotalPages(data.pagination?.totalPages ?? 1)
} catch (error) {
console.error('Failed to fetch clients:', error)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetch('/api/users?isActive=true&limit=200&fields=department')
.then((r) => r.json())
.then((d) => setUsers(d.users || []))
.catch(() => {})
}, [])
useEffect(() => {
fetchClients()
}, [page, search, designationFilter, advocateFilter, teamMemberFilter, showNoPolicies])
const handleSearchChange = (value: string) => {
setSearch(value)
setPage(1)
}
const handleFilterChange = (setter: (v: string) => void) => (value: string) => {
setter(value === '_all' ? '' : value)
setPage(1)
}
const hasActiveFilters = designationFilter || advocateFilter || teamMemberFilter
const handleToggleNoPolicies = () => {
setShowNoPolicies((v) => !v)
setPage(1)
}
return (
<div className="space-y-4">
{/* Filters */}
<div className="flex gap-3 flex-wrap">
<div className="flex-1 min-w-[200px]">
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search clients..."
value={search}
onChange={(e) => handleSearchChange(e.target.value)}
className="pl-10"
/>
</div>
</div>
<Select value={designationFilter || '_all'} onValueChange={handleFilterChange(setDesignationFilter)}>
<SelectTrigger className="w-[180px]">
<Filter className="mr-2 h-4 w-4" />
<SelectValue placeholder="All Designations" />
</SelectTrigger>
<SelectContent>
<SelectItem value="_all">All Designations</SelectItem>
{designations.map((d) => (
<SelectItem key={d.id} value={d.id}>{d.name}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={advocateFilter || '_all'} onValueChange={handleFilterChange(setAdvocateFilter)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="All Advocates" />
</SelectTrigger>
<SelectContent>
<SelectItem value="_all">All Advocates</SelectItem>
{users
.filter((u) => u.department?.toLowerCase().includes('claims'))
.map((u) => (
<SelectItem key={u.id} value={u.id}>{u.displayName || u.email}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={teamMemberFilter || '_all'} onValueChange={handleFilterChange(setTeamMemberFilter)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="All Team Members" />
</SelectTrigger>
<SelectContent>
<SelectItem value="_all">All Team Members</SelectItem>
{users.map((u) => (
<SelectItem key={u.id} value={u.id}>{u.displayName || u.email}</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant={showNoPolicies ? 'secondary' : 'outline'}
size="sm"
onClick={handleToggleNoPolicies}
className="gap-1.5 whitespace-nowrap"
title={showNoPolicies ? 'Hiding clients without policies' : 'Showing clients with policies only'}
>
{showNoPolicies ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />}
{showNoPolicies ? 'All Clients' : 'Has Policies'}
</Button>
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={() => {
setDesignationFilter('')
setAdvocateFilter('')
setTeamMemberFilter('')
setPage(1)
}}
>
<X className="h-4 w-4 mr-1" />
Clear filters
</Button>
)}
<div className="flex border rounded-md">
<Button
variant={viewMode === 'cards' ? 'default' : 'ghost'}
size="icon"
onClick={() => setViewMode('cards')}
title="Card view"
>
<LayoutGrid className="h-4 w-4" />
</Button>
<Button
variant={viewMode === 'table' ? 'default' : 'ghost'}
size="icon"
onClick={() => setViewMode('table')}
title="Table view"
>
<List className="h-4 w-4" />
</Button>
</div>
</div>
{/* Client List */}
{loading ? (
<div className="text-center py-8">Loading...</div>
) : clients.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No clients found
</div>
) : viewMode === 'cards' ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{clients.map((client) => (
<ClientCard key={client.id} client={client} />
))}
</div>
) : (
<ClientTable clients={clients} />
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex justify-center gap-2">
<Button
variant="outline"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
>
Previous
</Button>
<span className="flex items-center px-4">
Page {page} of {totalPages}
</span>
<Button
variant="outline"
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
>
Next
</Button>
</div>
)}
</div>
)
}