initial: standalone repo from monorepo split

This commit is contained in:
kevin-asprec
2026-04-13 09:36:37 +08:00
commit 3f5bf0e118
88 changed files with 10997 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
'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>
);
}