250 lines
8 KiB
TypeScript
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>
|
|
)
|
|
}
|