feat(hq-web): clients — status filter chips, mono codes, skeleton/error states

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/client-detail-redesign
Thomas Joise 5 days ago
parent 00ce1d657e
commit 9aa376fa2b

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { Badge, Button, DataTable, EmptyState, Field, Notice, PageHeader, Toolbar } from '@sims/ui'
import { createClient, getClients, type ClientStatus, type DocStatus } from '../api'
import { Badge, Button, DataTable, EmptyState, ErrorState, Field, FilterChips, Notice, PageHeader, Skeleton, Toolbar } from '@sims/ui'
import { createClient, getClients, CLIENT_STATUSES, type ClientStatus, type DocStatus } from '../api'
/** Tiny fetch-render hook shared by the HQ pages (same shape as backoffice live.tsx). */
export function useData<T>(loader: () => Promise<T>, deps: unknown[] = []): {
@ -36,6 +36,10 @@ export function Clients() {
const { data, error, reload } = useData(() => getClients(q), [q])
const [sp] = useSearchParams()
const [creating, setCreating] = useState(sp.get('new') === '1')
const [statusFilter, setStatusFilter] = useState('all')
const counts = new Map<string, number>()
for (const c of data ?? []) counts.set(c.status, (counts.get(c.status) ?? 0) + 1)
const shown = (data ?? []).filter((c) => statusFilter === 'all' || c.status === statusFilter)
return (
<div className="wf-page">
@ -49,22 +53,31 @@ export function Clients() {
className="wf" style={{ maxWidth: 280 }} placeholder="Search name / code / GSTIN…"
value={q} onChange={(e) => setQ(e.target.value)}
/>
<Badge>{data?.length ?? '…'} clients</Badge>
<FilterChips
active={statusFilter}
onChange={setStatusFilter}
chips={[
{ key: 'all', label: 'All', count: data?.length ?? 0 },
...CLIENT_STATUSES.map((s) => ({ key: s, label: s, count: counts.get(s) ?? 0 })),
]}
/>
<span className="spacer" />
<Badge>{shown.length} shown</Badge>
</Toolbar>
{creating && (
<NewClientForm onCreated={(id) => { setCreating(false); reload(); nav(`/clients/${id}`) }} />
)}
{error !== undefined && <Notice tone="err">{error}</Notice>}
{data === undefined ? <EmptyState>Loading</EmptyState>
: data.length === 0 ? <EmptyState>No clients{q !== '' ? ` matching “${q}` : ' yet — add the first one'}.</EmptyState> : (
{error !== undefined && <ErrorState message={error} onRetry={reload} />}
{data === undefined && error === undefined ? <Skeleton rows={6} />
: shown.length === 0 ? <EmptyState>No clients{q !== '' ? ` matching “${q}` : statusFilter !== 'all' ? ` with status ${statusFilter}` : ' yet — add the first one'}.</EmptyState> : (
<DataTable
columns={[
{ key: 'code', label: 'Code' }, { key: 'name', label: 'Name' },
{ key: 'code', label: 'Code', mono: true }, { key: 'name', label: 'Name' },
{ key: 'status', label: 'Status' }, { key: 'state', label: 'State' },
{ key: 'contact', label: 'Contact' },
]}
onRowClick={(_row, i) => nav(`/clients/${data[i]!.id}`)}
rows={data.map((c) => ({
onRowClick={(_row, i) => nav(`/clients/${shown[i]!.id}`)}
rows={shown.map((c) => ({
code: c.code, name: c.name,
status: <Badge tone={CLIENT_TONE[c.status]}>{c.status}</Badge>,
state: c.stateCode,

Loading…
Cancel
Save