96 lines
4.1 KiB
TypeScript
96 lines
4.1 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import Link from 'next/link';
|
|
import { useRouter } from 'next/navigation';
|
|
import { api } from '@/lib/api';
|
|
import { PageHeader } from '@/components/ui/page-header';
|
|
import { DataTable } from '@/components/ui/data-table';
|
|
import { Badge, statusBadgeVariant } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { ActionIcon } from '@/components/ui/action-icon';
|
|
import { CreateClientModal } from '@/components/modals/create-client-modal';
|
|
|
|
interface Client {
|
|
id: string;
|
|
accountNumber: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
email: string | null;
|
|
phone: string | null;
|
|
status: string;
|
|
area: { id: string; name: string } | null;
|
|
_count: { subscriptions: number; tickets: number };
|
|
}
|
|
|
|
export default function ClientsPage() {
|
|
const router = useRouter();
|
|
const [clients, setClients] = useState<Client[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [search, setSearch] = useState('');
|
|
const [showCreate, setShowCreate] = useState(false);
|
|
const [filters, setFilters] = useState<Record<string, string>>({});
|
|
|
|
const loadClients = useCallback(async () => {
|
|
try {
|
|
const res = await api.get('/clients?limit=100');
|
|
const d = res.data.data;
|
|
setClients(Array.isArray(d) ? d : d.items);
|
|
} finally { setLoading(false); }
|
|
}, []);
|
|
|
|
useEffect(() => { loadClients(); }, [loadClients]);
|
|
|
|
const filtered = clients.filter((c) => {
|
|
if (search && !`${c.firstName} ${c.lastName} ${c.accountNumber} ${c.phone || ''} ${c.email || ''}`.toLowerCase().includes(search.toLowerCase())) return false;
|
|
if (filters.status && c.status !== filters.status) return false;
|
|
return true;
|
|
});
|
|
|
|
return (
|
|
<div>
|
|
<PageHeader title="Clients" description="Manage subscriber accounts"
|
|
action={<Button onClick={() => setShowCreate(true)}>New Client</Button>} />
|
|
|
|
<div className="mt-5">
|
|
<DataTable
|
|
data={filtered}
|
|
loading={loading}
|
|
keyExtractor={(c) => c.id}
|
|
emptyTitle="No clients found"
|
|
emptyDescription="Create your first client to get started."
|
|
searchPlaceholder="Search by name, account #, phone, email..."
|
|
searchValue={search}
|
|
onSearchChange={setSearch}
|
|
quickFilters={[
|
|
{ key: 'status', label: 'Status', options: [
|
|
{ label: 'Active', value: 'active' },
|
|
{ label: 'Inactive', value: 'inactive' },
|
|
{ label: 'Suspended', value: 'suspended' },
|
|
]},
|
|
]}
|
|
activeFilters={filters}
|
|
onFilterChange={(k, v) => setFilters((f) => ({ ...f, [k]: v }))}
|
|
onRowClick={(c) => router.push(`/dashboard/clients/${c.id}`)}
|
|
columns={[
|
|
{ key: 'accountNumber', label: 'Account', sortable: true, render: (c) => <span className="font-mono text-surface-700 dark:text-surface-300">{c.accountNumber}</span> },
|
|
{ key: 'firstName', label: 'Name', sortable: true, render: (c) => (
|
|
<Link href={`/dashboard/clients/${c.id}`} className="font-medium text-surface-800 dark:text-surface-200 hover:text-primary-600 transition-colors">{c.firstName} {c.lastName}</Link>
|
|
)},
|
|
{ key: 'area', label: 'Area', render: (c) => <span className="text-surface-500 dark:text-surface-400">{c.area?.name || '—'}</span> },
|
|
{ key: 'phone', label: 'Contact', render: (c) => <span className="text-surface-500 dark:text-surface-400">{c.phone || c.email || '—'}</span> },
|
|
{ key: 'status', label: 'Status', sortable: true, render: (c) => <Badge label={c.status} variant={statusBadgeVariant(c.status)} /> },
|
|
{ key: 'actions', label: '', align: 'right', render: (c) => (
|
|
<Link href={`/dashboard/clients/${c.id}`} onClick={(e) => e.stopPropagation()}>
|
|
<ActionIcon icon="eye" variant="ghost" label="View details" />
|
|
</Link>
|
|
)},
|
|
]}
|
|
/>
|
|
</div>
|
|
|
|
<CreateClientModal open={showCreate} onClose={() => setShowCreate(false)} onSuccess={loadClients} />
|
|
</div>
|
|
);
|
|
}
|