120 lines
3.8 KiB
TypeScript
120 lines
3.8 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { api, type BusinessList, type BusinessStatus } from '../api.js';
|
|
|
|
const TABS: { status: BusinessStatus; label: string }[] = [
|
|
{ status: 'ACTIVE', label: 'Active' },
|
|
{ status: 'SOLD', label: 'Sold' },
|
|
{ status: 'INACTIVE', label: 'Inactive' },
|
|
];
|
|
|
|
export default function Businesses({ onOpen }: { onOpen: (id: string) => void }) {
|
|
const [tab, setTab] = useState<BusinessStatus>('ACTIVE');
|
|
const [search, setSearch] = useState('');
|
|
const [data, setData] = useState<BusinessList | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [scanning, setScanning] = useState(false);
|
|
const [scanMsg, setScanMsg] = useState<string | null>(null);
|
|
const [reload, setReload] = useState(0);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
api
|
|
.businesses(tab, search)
|
|
.then((res) => {
|
|
if (!cancelled) {
|
|
setData(res);
|
|
setError(null);
|
|
}
|
|
})
|
|
.catch((err: Error) => !cancelled && setError(err.message));
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [tab, search, reload]);
|
|
|
|
async function scan() {
|
|
setScanning(true);
|
|
setScanMsg(null);
|
|
try {
|
|
const res = await api.scan();
|
|
setScanMsg(
|
|
`Scanned ${res.scanned} · inserted ${res.inserted} · updated ${res.updated} · missing ${res.missing}`,
|
|
);
|
|
setReload((n) => n + 1);
|
|
setTimeout(() => setScanMsg(null), 8000);
|
|
} catch (err) {
|
|
setScanMsg((err as Error).message);
|
|
} finally {
|
|
setScanning(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-4 flex items-center justify-between gap-3">
|
|
<div className="flex gap-1">
|
|
{TABS.map((t) => (
|
|
<button
|
|
key={t.status}
|
|
onClick={() => setTab(t.status)}
|
|
className={`rounded px-3 py-1.5 text-sm ${
|
|
tab === t.status
|
|
? 'bg-gray-900 text-white'
|
|
: 'border border-gray-300 bg-white hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
{t.label} ({data?.counts[t.status] ?? 0})
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
placeholder="Search name…"
|
|
className="w-56 rounded border border-gray-300 px-2 py-1.5 text-sm"
|
|
/>
|
|
<button
|
|
onClick={scan}
|
|
disabled={scanning}
|
|
className="rounded bg-gray-900 px-3 py-1.5 text-sm text-white disabled:opacity-50"
|
|
>
|
|
{scanning ? 'Scanning…' : 'Scan NAS now'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{scanMsg && <p className="mb-3 text-sm text-gray-600">{scanMsg}</p>}
|
|
{error && <p className="mb-3 text-sm text-red-600">{error}</p>}
|
|
|
|
<table className="w-full border-collapse bg-white text-sm">
|
|
<thead>
|
|
<tr className="border-b border-gray-200 text-left text-xs uppercase tracking-wide text-gray-500">
|
|
<th className="px-3 py-2 font-medium">Name</th>
|
|
<th className="w-32 px-3 py-2 font-medium">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data?.businesses.map((b) => (
|
|
<tr
|
|
key={b.id}
|
|
onClick={() => onOpen(b.id)}
|
|
className="cursor-pointer border-b border-gray-100 hover:bg-gray-50"
|
|
>
|
|
<td className="px-3 py-1.5">{b.name}</td>
|
|
<td className="px-3 py-1.5 text-gray-500">{b.status}</td>
|
|
</tr>
|
|
))}
|
|
{data && data.businesses.length === 0 && (
|
|
<tr>
|
|
<td colSpan={2} className="px-3 py-4 text-gray-500">
|
|
No businesses.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|