phase 16: React dashboard with full platform control and analytics
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
useTradingConfig,
|
||||
useSetTradingMode,
|
||||
usePendingApprovals,
|
||||
useReviewApproval,
|
||||
useActiveLockouts,
|
||||
} from '../api/hooks';
|
||||
import { StatusBadge, LoadingSpinner, Card } from '../components/ui';
|
||||
|
||||
export function TradingPage() {
|
||||
const { data: config, isLoading: configLoading } = useTradingConfig();
|
||||
const { data: approvals } = usePendingApprovals();
|
||||
const { data: lockouts } = useActiveLockouts();
|
||||
const setMode = useSetTradingMode();
|
||||
const reviewApproval = useReviewApproval();
|
||||
const [confirmMode, setConfirmMode] = useState<string | null>(null);
|
||||
|
||||
if (configLoading) return <LoadingSpinner />;
|
||||
|
||||
const currentMode = config?.trading_mode ?? 'paper';
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-xl font-semibold text-gray-100">Trading Controls</h1>
|
||||
|
||||
{/* Trading Mode */}
|
||||
<Card>
|
||||
<h2 className="mb-3 text-sm font-medium text-gray-400">Trading Mode</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
{['paper', 'live', 'disabled'].map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
onClick={() => {
|
||||
if (mode === currentMode) return;
|
||||
if (mode === 'live') { setConfirmMode(mode); return; }
|
||||
setMode.mutate(mode);
|
||||
}}
|
||||
className={`rounded-md px-4 py-2 text-sm font-medium capitalize transition-colors ${
|
||||
currentMode === mode
|
||||
? 'bg-brand-600 text-white'
|
||||
: 'border border-surface-700 bg-surface-900 text-gray-400 hover:bg-surface-800'
|
||||
}`}
|
||||
aria-pressed={currentMode === mode}
|
||||
>
|
||||
{mode}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Confirmation dialog for live mode */}
|
||||
{confirmMode && (
|
||||
<div className="mt-4 rounded-lg border border-orange-700/50 bg-orange-900/20 p-4">
|
||||
<p className="text-sm text-orange-300">
|
||||
Are you sure you want to switch to <span className="font-semibold">{confirmMode}</span> mode?
|
||||
This enables real order execution.
|
||||
</p>
|
||||
<div className="mt-3 flex gap-2">
|
||||
<button
|
||||
onClick={() => { setMode.mutate(confirmMode); setConfirmMode(null); }}
|
||||
className="rounded-md bg-orange-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-orange-700"
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmMode(null)}
|
||||
className="rounded-md border border-surface-700 px-3 py-1.5 text-sm text-gray-400 hover:bg-surface-800"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Pending Approvals */}
|
||||
<Card>
|
||||
<h2 className="mb-3 text-sm font-medium text-gray-400">
|
||||
Pending Approvals ({approvals?.length ?? 0})
|
||||
</h2>
|
||||
{!approvals?.length ? (
|
||||
<p className="text-sm text-gray-500">No pending approvals</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{approvals.map((a) => (
|
||||
<ApprovalRow key={a.id} approval={a} onReview={(approved, note) => reviewApproval.mutate({ id: a.id, approved, review_note: note })} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Active Lockouts */}
|
||||
<Card>
|
||||
<h2 className="mb-3 text-sm font-medium text-gray-400">
|
||||
Active Lockouts ({lockouts?.length ?? 0})
|
||||
</h2>
|
||||
{!lockouts?.length ? (
|
||||
<p className="text-sm text-gray-500">No active lockouts</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{lockouts.map((l) => {
|
||||
const expiresIn = l.expires_at ? Math.max(0, Math.round((new Date(l.expires_at).getTime() - Date.now()) / 60000)) : 0;
|
||||
return (
|
||||
<div key={l.id} className="flex items-center justify-between rounded border border-surface-700 bg-surface-950 p-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono font-semibold text-brand-300">{l.ticker}</span>
|
||||
<StatusBadge status={l.lockout_type} />
|
||||
<span className="text-sm text-gray-400">{l.reason}</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
{expiresIn > 0 ? `${expiresIn}m remaining` : 'expired'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ApprovalRow({ approval, onReview }: {
|
||||
approval: { id: string; ticker: string; side: string; quantity: number; estimated_value: number | null; requested_at: string };
|
||||
onReview: (approved: boolean, note: string) => void;
|
||||
}) {
|
||||
const [note, setNote] = useState('');
|
||||
|
||||
return (
|
||||
<div className="rounded border border-surface-700 bg-surface-950 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono font-semibold text-brand-300">{approval.ticker}</span>
|
||||
<StatusBadge status={approval.side} />
|
||||
<span className="text-sm text-gray-300">qty: {approval.quantity}</span>
|
||||
{approval.estimated_value != null && (
|
||||
<span className="text-sm text-gray-500">${approval.estimated_value.toFixed(2)}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">{new Date(approval.requested_at).toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Review note…"
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
className="flex-1 rounded-md border border-surface-700 bg-surface-900 px-2 py-1 text-xs text-gray-200 placeholder-gray-500"
|
||||
aria-label="Review note"
|
||||
/>
|
||||
<button onClick={() => onReview(true, note)} className="rounded bg-green-700 px-3 py-1 text-xs font-medium text-white hover:bg-green-600">
|
||||
Approve
|
||||
</button>
|
||||
<button onClick={() => onReview(false, note)} className="rounded bg-red-700 px-3 py-1 text-xs font-medium text-white hover:bg-red-600">
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user