phase 16: React dashboard with full platform control and analytics

This commit is contained in:
Celes Renata
2026-04-11 16:19:46 -07:00
parent 25e0e386b7
commit faccb0b8db
53 changed files with 7924 additions and 13 deletions
+190
View File
@@ -0,0 +1,190 @@
import { useParams } from '@tanstack/react-router';
import { useState } from 'react';
import { useCompany, useCompanySources, useCreateAlias, useCreateSource } from '../api/hooks';
import { StatusBadge, LoadingSpinner, Card } from '../components/ui';
import { DataTable, type Column } from '../components/DataTable';
import type { Source } from '../api/hooks';
import type { Alias } from '../api/hooks';
const sourceCols: Column<Source>[] = [
{ key: 'source_type', header: 'Type' },
{ key: 'source_name', header: 'Name' },
{ key: 'credibility_score', header: 'Credibility', render: (r) => <span>{(r.credibility_score * 100).toFixed(0)}%</span> },
{ key: 'active', header: 'Status', render: (r) => <StatusBadge status={r.active ? 'active' : 'disabled'} /> },
];
export function CompanyDetailPage() {
const { id } = useParams({ from: '/companies/$id' });
const { data: company, isLoading } = useCompany(id);
const { data: sources } = useCompanySources(id);
const [tab, setTab] = useState<'aliases' | 'sources'>('sources');
if (isLoading || !company) return <LoadingSpinner />;
return (
<div className="space-y-6">
<div className="flex items-center gap-3">
<h1 className="text-xl font-semibold text-gray-100">{company.ticker}</h1>
<StatusBadge status={company.active ? 'active' : 'disabled'} />
</div>
<Card>
<dl className="grid grid-cols-2 gap-x-8 gap-y-2 text-sm sm:grid-cols-4">
<div><dt className="text-gray-500">Name</dt><dd className="text-gray-200">{company.legal_name}</dd></div>
<div><dt className="text-gray-500">Exchange</dt><dd className="text-gray-200">{company.exchange ?? '—'}</dd></div>
<div><dt className="text-gray-500">Sector</dt><dd className="text-gray-200">{company.sector ?? '—'}</dd></div>
<div><dt className="text-gray-500">Industry</dt><dd className="text-gray-200">{company.industry ?? '—'}</dd></div>
<div><dt className="text-gray-500">Market Cap</dt><dd className="text-gray-200">{company.market_cap_bucket ?? '—'}</dd></div>
<div><dt className="text-gray-500">Sources</dt><dd className="text-gray-200">{company.active_source_count ?? 0}</dd></div>
</dl>
</Card>
{/* Tabs */}
<div className="flex gap-4 border-b border-surface-700">
{(['sources', 'aliases'] as const).map((t) => (
<button
key={t}
onClick={() => setTab(t)}
className={`pb-2 text-sm font-medium capitalize transition-colors ${tab === t ? 'border-b-2 border-brand-500 text-brand-300' : 'text-gray-500 hover:text-gray-300'}`}
>
{t}
</button>
))}
</div>
{tab === 'sources' && (
<div className="space-y-4">
<DataTable<Source> data={sources ?? []} columns={sourceCols} keyField="id" />
<AddSourceForm companyId={id} />
</div>
)}
{tab === 'aliases' && (
<div className="space-y-4">
<AliasesList aliases={company.aliases ?? []} />
<AddAliasForm companyId={id} />
</div>
)}
</div>
);
}
function AliasesList({ aliases }: { aliases: Alias[] }) {
if (aliases.length === 0) return <p className="text-sm text-gray-500">No aliases configured</p>;
return (
<div className="flex flex-wrap gap-2">
{aliases.map((a) => (
<span key={a.id} className="rounded-full border border-surface-700 bg-surface-800 px-3 py-1 text-xs text-gray-300">
{a.alias} <span className="text-gray-500">({a.alias_type})</span>
</span>
))}
</div>
);
}
function AddAliasForm({ companyId }: { companyId: string }) {
const [alias, setAlias] = useState('');
const mutation = useCreateAlias(companyId);
return (
<form
className="flex gap-2"
onSubmit={(e) => {
e.preventDefault();
if (alias.trim()) mutation.mutate({ alias: alias.trim() }, { onSuccess: () => setAlias('') });
}}
>
<input
type="text"
placeholder="Add alias…"
value={alias}
onChange={(e) => setAlias(e.target.value)}
className="rounded-md border border-surface-700 bg-surface-900 px-3 py-1.5 text-sm text-gray-200 placeholder-gray-500 focus:border-brand-500 focus:outline-none"
aria-label="New alias"
/>
<button type="submit" disabled={mutation.isPending} className="rounded-md bg-brand-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-brand-700 disabled:opacity-50">
Add
</button>
</form>
);
}
function AddSourceForm({ companyId }: { companyId: string }) {
const [open, setOpen] = useState(false);
const [sourceType, setSourceType] = useState('market_api');
const [sourceName, setSourceName] = useState('');
const [credibility, setCredibility] = useState(0.5);
const mutation = useCreateSource(companyId);
if (!open) {
return (
<button onClick={() => setOpen(true)} className="rounded-md bg-brand-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-brand-700">
Add Source
</button>
);
}
return (
<Card>
<form
className="space-y-3"
onSubmit={(e) => {
e.preventDefault();
mutation.mutate(
{ source_type: sourceType, source_name: sourceName, credibility_score: credibility },
{ onSuccess: () => { setOpen(false); setSourceName(''); } },
);
}}
>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="mb-1 block text-xs text-gray-500" htmlFor="source-type">Type</label>
<select
id="source-type"
value={sourceType}
onChange={(e) => setSourceType(e.target.value)}
className="w-full rounded-md border border-surface-700 bg-surface-900 px-2 py-1.5 text-sm text-gray-200"
>
{['market_api', 'news_api', 'filings_api', 'web_scrape', 'broker'].map((t) => (
<option key={t} value={t}>{t}</option>
))}
</select>
</div>
<div>
<label className="mb-1 block text-xs text-gray-500" htmlFor="source-name">Name</label>
<input
id="source-name"
type="text"
value={sourceName}
onChange={(e) => setSourceName(e.target.value)}
className="w-full rounded-md border border-surface-700 bg-surface-900 px-2 py-1.5 text-sm text-gray-200 placeholder-gray-500"
placeholder="Source name"
required
/>
</div>
</div>
<div>
<label className="mb-1 block text-xs text-gray-500" htmlFor="credibility">Credibility: {(credibility * 100).toFixed(0)}%</label>
<input
id="credibility"
type="range"
min={0}
max={1}
step={0.05}
value={credibility}
onChange={(e) => setCredibility(Number(e.target.value))}
className="w-full"
/>
</div>
<div className="flex gap-2">
<button type="submit" disabled={mutation.isPending} className="rounded-md bg-brand-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-brand-700 disabled:opacity-50">
Save
</button>
<button type="button" onClick={() => setOpen(false)} className="rounded-md border border-surface-700 px-3 py-1.5 text-sm text-gray-400 hover:bg-surface-800">
Cancel
</button>
</div>
</form>
</Card>
);
}