feat: add Reports page and detail view to frontend
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-2 Pipeline was successful
ci/woodpecker/push/build-1 Pipeline was successful
ci/woodpecker/push/build-3 Pipeline was successful
ci/woodpecker/push/finalize Pipeline was successful
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.adapters.broker_adapter name:broker-adapter]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.aggregation.worker name:aggregation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.extractor.worker name:extractor]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.ingestion.worker name:ingestion]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.lake_publisher.worker name:lake-publisher]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.parser.worker name:parser]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.recommendation.worker name:recommendation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.scheduler.app name:scheduler]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.api.app:app --host 0.0.0.0 --port 8000 name:query-api]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.risk.app:app --host 0.0.0.0 --port 8000 name:risk]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.symbol_registry.app:app --host 0.0.0.0 --port 8000 name:symbol-registry]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.trading.app:app --host 0.0.0.0 --port 8000 name:trading-engine]) (push) Has been cancelled
Build and Push / build-dashboard (push) Has been cancelled
Build and Push / build-superset (push) Has been cancelled
Build and Push / integration-test (push) Has been cancelled
Build and Push / beta-gate (push) Has been cancelled
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-2 Pipeline was successful
ci/woodpecker/push/build-1 Pipeline was successful
ci/woodpecker/push/build-3 Pipeline was successful
ci/woodpecker/push/finalize Pipeline was successful
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.adapters.broker_adapter name:broker-adapter]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.aggregation.worker name:aggregation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.extractor.worker name:extractor]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.ingestion.worker name:ingestion]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.lake_publisher.worker name:lake-publisher]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.parser.worker name:parser]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.recommendation.worker name:recommendation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.scheduler.app name:scheduler]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.api.app:app --host 0.0.0.0 --port 8000 name:query-api]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.risk.app:app --host 0.0.0.0 --port 8000 name:risk]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.symbol_registry.app:app --host 0.0.0.0 --port 8000 name:symbol-registry]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.trading.app:app --host 0.0.0.0 --port 8000 name:trading-engine]) (push) Has been cancelled
Build and Push / build-dashboard (push) Has been cancelled
Build and Push / build-superset (push) Has been cancelled
Build and Push / integration-test (push) Has been cancelled
Build and Push / beta-gate (push) Has been cancelled
- Reports list page with type filter (daily/weekly) - Report detail page with all sections: P&L, recommendation accuracy, position performance table, risk metrics, model quality windows - Executive summary card, validation warnings display - Nav item under Trading group - Routes: /reports and /reports/:id
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
|||||||
Globe,
|
Globe,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Bot,
|
Bot,
|
||||||
|
ClipboardList,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
@@ -40,6 +41,7 @@ const navItems: NavItem[] = [
|
|||||||
{ to: '/positions', label: 'Positions', icon: <Wallet size={18} />, group: 'Trading' },
|
{ to: '/positions', label: 'Positions', icon: <Wallet size={18} />, group: 'Trading' },
|
||||||
{ to: '/trading', label: 'Trading Controls', icon: <ShieldCheck size={18} />, group: 'Trading' },
|
{ to: '/trading', label: 'Trading Controls', icon: <ShieldCheck size={18} />, group: 'Trading' },
|
||||||
{ to: '/trading/engine', label: 'Trading Engine', icon: <BarChart3 size={18} />, group: 'Trading' },
|
{ to: '/trading/engine', label: 'Trading Engine', icon: <BarChart3 size={18} />, group: 'Trading' },
|
||||||
|
{ to: '/reports', label: 'Reports', icon: <ClipboardList size={18} />, group: 'Trading' },
|
||||||
{ to: '/ops/pipeline', label: 'Pipeline', icon: <Activity size={18} />, group: 'Ops' },
|
{ to: '/ops/pipeline', label: 'Pipeline', icon: <Activity size={18} />, group: 'Ops' },
|
||||||
{ to: '/ops/ingestion', label: 'Ingestion', icon: <Download size={18} />, group: 'Ops' },
|
{ to: '/ops/ingestion', label: 'Ingestion', icon: <Download size={18} />, group: 'Ops' },
|
||||||
{ to: '/ops/model', label: 'Model Perf', icon: <Cpu size={18} />, group: 'Ops' },
|
{ to: '/ops/model', label: 'Model Perf', icon: <Cpu size={18} />, group: 'Ops' },
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
import { useParams, Link } from '@tanstack/react-router';
|
||||||
|
import { useReport } from '../api/hooks';
|
||||||
|
import { LoadingSpinner, StatusBadge, Card } from '../components/ui';
|
||||||
|
import { ArrowLeft } from 'lucide-react';
|
||||||
|
|
||||||
|
interface PLSection {
|
||||||
|
realized_pnl: number;
|
||||||
|
unrealized_pnl: number;
|
||||||
|
daily_return: number;
|
||||||
|
cumulative_return: number;
|
||||||
|
win_count: number;
|
||||||
|
loss_count: number;
|
||||||
|
win_rate: number;
|
||||||
|
profit_factor: number;
|
||||||
|
sharpe_ratio: number;
|
||||||
|
summary: string;
|
||||||
|
validation_warnings?: { field_name: string; computed_value: number; snapshot_value: number; pct_difference: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PositionDetail {
|
||||||
|
ticker: string;
|
||||||
|
entry_price: number;
|
||||||
|
current_or_exit_price: number;
|
||||||
|
pnl: number;
|
||||||
|
pnl_pct: number;
|
||||||
|
hold_duration_hours: number;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RiskMetrics {
|
||||||
|
current_risk_tier: string;
|
||||||
|
portfolio_heat: number;
|
||||||
|
max_drawdown: number;
|
||||||
|
current_drawdown_pct: number;
|
||||||
|
reserve_pool_balance: number;
|
||||||
|
circuit_breaker_event_count: number;
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelWindow {
|
||||||
|
lookback: string;
|
||||||
|
win_rate: number | null;
|
||||||
|
directional_accuracy: number | null;
|
||||||
|
information_coefficient: number | null;
|
||||||
|
calibration_error: number | null;
|
||||||
|
brier_score: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReportData {
|
||||||
|
pnl: PLSection;
|
||||||
|
recommendation_accuracy: {
|
||||||
|
total_evaluated: number;
|
||||||
|
act_count: number;
|
||||||
|
skip_count: number;
|
||||||
|
acted_win_rate: number;
|
||||||
|
avg_confidence_acted: number;
|
||||||
|
avg_confidence_skipped: number;
|
||||||
|
summary: string;
|
||||||
|
validation_warnings?: { field_name: string; pct_difference: number }[];
|
||||||
|
};
|
||||||
|
position_performance: {
|
||||||
|
positions: PositionDetail[];
|
||||||
|
summary: string;
|
||||||
|
};
|
||||||
|
risk_metrics: RiskMetrics;
|
||||||
|
model_quality: {
|
||||||
|
windows: ModelWindow[];
|
||||||
|
summary: string;
|
||||||
|
validation_warnings?: { field_name: string; pct_difference: number }[];
|
||||||
|
};
|
||||||
|
executive_summary: string;
|
||||||
|
validation_status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MetricCard({ label, value, sub }: { label: string; value: string; sub?: string }) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg bg-surface-800 border border-surface-700 p-3">
|
||||||
|
<div className="text-xs text-gray-400 mb-1">{label}</div>
|
||||||
|
<div className="text-lg font-semibold text-gray-100">{value}</div>
|
||||||
|
{sub && <div className="text-xs text-gray-500 mt-0.5">{sub}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pct(v: number) {
|
||||||
|
return `${(v * 100).toFixed(2)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dollar(v: number) {
|
||||||
|
return v >= 0 ? `$${v.toFixed(2)}` : `-$${Math.abs(v).toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReportDetailPage() {
|
||||||
|
const { id } = useParams({ from: '/reports/$id' });
|
||||||
|
const { data, isLoading } = useReport(id);
|
||||||
|
|
||||||
|
if (isLoading) return <LoadingSpinner />;
|
||||||
|
if (!data) return <div className="text-gray-400">Report not found</div>;
|
||||||
|
|
||||||
|
const report = data.report_data as unknown as ReportData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Link to="/reports" className="text-gray-400 hover:text-gray-200">
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
</Link>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-semibold text-gray-100">
|
||||||
|
{data.report_type === 'daily' ? 'Daily' : 'Weekly'} Report
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
{data.period_start === data.period_end
|
||||||
|
? data.period_start
|
||||||
|
: `${data.period_start} → ${data.period_end}`}
|
||||||
|
{' · '}
|
||||||
|
<StatusBadge status={data.validation_status} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Executive Summary */}
|
||||||
|
{report.executive_summary && (
|
||||||
|
<Card>
|
||||||
|
<h2 className="text-sm font-medium text-gray-300 mb-2">Executive Summary</h2>
|
||||||
|
<p className="text-sm text-gray-200 whitespace-pre-wrap leading-relaxed">
|
||||||
|
{report.executive_summary}
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* P&L Section */}
|
||||||
|
<Card>
|
||||||
|
<h2 className="text-sm font-medium text-gray-300 mb-3">P&L</h2>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-3">
|
||||||
|
<MetricCard label="Realized P&L" value={dollar(report.pnl.realized_pnl)} />
|
||||||
|
<MetricCard label="Unrealized P&L" value={dollar(report.pnl.unrealized_pnl)} />
|
||||||
|
<MetricCard label="Daily Return" value={pct(report.pnl.daily_return)} />
|
||||||
|
<MetricCard label="Cumulative Return" value={pct(report.pnl.cumulative_return)} />
|
||||||
|
<MetricCard label="Win Rate" value={pct(report.pnl.win_rate)} sub={`${report.pnl.win_count}W / ${report.pnl.loss_count}L`} />
|
||||||
|
<MetricCard label="Profit Factor" value={report.pnl.profit_factor.toFixed(2)} />
|
||||||
|
<MetricCard label="Sharpe Ratio" value={report.pnl.sharpe_ratio.toFixed(2)} />
|
||||||
|
</div>
|
||||||
|
{report.pnl.summary && (
|
||||||
|
<p className="text-xs text-gray-400 mt-2">{report.pnl.summary}</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Recommendation Accuracy */}
|
||||||
|
<Card>
|
||||||
|
<h2 className="text-sm font-medium text-gray-300 mb-3">Recommendation Accuracy</h2>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-3">
|
||||||
|
<MetricCard label="Total Evaluated" value={String(report.recommendation_accuracy.total_evaluated)} />
|
||||||
|
<MetricCard label="Acted" value={String(report.recommendation_accuracy.act_count)} />
|
||||||
|
<MetricCard label="Skipped" value={String(report.recommendation_accuracy.skip_count)} />
|
||||||
|
<MetricCard label="Acted Win Rate" value={pct(report.recommendation_accuracy.acted_win_rate)} />
|
||||||
|
<MetricCard label="Avg Confidence (Acted)" value={report.recommendation_accuracy.avg_confidence_acted.toFixed(3)} />
|
||||||
|
<MetricCard label="Avg Confidence (Skipped)" value={report.recommendation_accuracy.avg_confidence_skipped.toFixed(3)} />
|
||||||
|
</div>
|
||||||
|
{report.recommendation_accuracy.validation_warnings && report.recommendation_accuracy.validation_warnings.length > 0 && (
|
||||||
|
<div className="mt-2 rounded bg-yellow-900/20 border border-yellow-700/30 p-2">
|
||||||
|
<span className="text-xs text-yellow-400">⚠ Validation warnings:</span>
|
||||||
|
{report.recommendation_accuracy.validation_warnings.map((w, i) => (
|
||||||
|
<span key={i} className="text-xs text-yellow-300 ml-2">{w.field_name} ({w.pct_difference.toFixed(1)}% off)</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{report.recommendation_accuracy.summary && (
|
||||||
|
<p className="text-xs text-gray-400 mt-2">{report.recommendation_accuracy.summary}</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Position Performance */}
|
||||||
|
<Card>
|
||||||
|
<h2 className="text-sm font-medium text-gray-300 mb-3">
|
||||||
|
Positions ({report.position_performance.positions.length})
|
||||||
|
</h2>
|
||||||
|
{report.position_performance.positions.length > 0 ? (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-left text-xs text-gray-400 border-b border-surface-700">
|
||||||
|
<th className="pb-2 pr-4">Ticker</th>
|
||||||
|
<th className="pb-2 pr-4">Status</th>
|
||||||
|
<th className="pb-2 pr-4">Entry</th>
|
||||||
|
<th className="pb-2 pr-4">Current/Exit</th>
|
||||||
|
<th className="pb-2 pr-4">P&L</th>
|
||||||
|
<th className="pb-2 pr-4">P&L %</th>
|
||||||
|
<th className="pb-2">Hold (hrs)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{report.position_performance.positions.map((p, i) => (
|
||||||
|
<tr key={i} className="border-b border-surface-800 text-gray-200">
|
||||||
|
<td className="py-1.5 pr-4 font-mono font-semibold text-brand-300">{p.ticker}</td>
|
||||||
|
<td className="py-1.5 pr-4"><StatusBadge status={p.status} /></td>
|
||||||
|
<td className="py-1.5 pr-4">${p.entry_price.toFixed(2)}</td>
|
||||||
|
<td className="py-1.5 pr-4">${p.current_or_exit_price.toFixed(2)}</td>
|
||||||
|
<td className={`py-1.5 pr-4 font-mono ${p.pnl >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{dollar(p.pnl)}
|
||||||
|
</td>
|
||||||
|
<td className={`py-1.5 pr-4 ${p.pnl_pct >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{p.pnl_pct.toFixed(2)}%
|
||||||
|
</td>
|
||||||
|
<td className="py-1.5 text-gray-400">{p.hold_duration_hours.toFixed(1)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-500">No positions during this period.</p>
|
||||||
|
)}
|
||||||
|
{report.position_performance.summary && (
|
||||||
|
<p className="text-xs text-gray-400 mt-3">{report.position_performance.summary}</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Risk Metrics */}
|
||||||
|
<Card>
|
||||||
|
<h2 className="text-sm font-medium text-gray-300 mb-3">Risk Metrics</h2>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||||
|
<MetricCard label="Risk Tier" value={report.risk_metrics.current_risk_tier} />
|
||||||
|
<MetricCard label="Portfolio Heat" value={pct(report.risk_metrics.portfolio_heat)} />
|
||||||
|
<MetricCard label="Max Drawdown" value={pct(report.risk_metrics.max_drawdown)} />
|
||||||
|
<MetricCard label="Current Drawdown" value={pct(report.risk_metrics.current_drawdown_pct)} />
|
||||||
|
<MetricCard label="Reserve Pool" value={dollar(report.risk_metrics.reserve_pool_balance)} />
|
||||||
|
<MetricCard label="Circuit Breaker Events" value={String(report.risk_metrics.circuit_breaker_event_count)} />
|
||||||
|
</div>
|
||||||
|
{report.risk_metrics.summary && (
|
||||||
|
<p className="text-xs text-gray-400 mt-3">{report.risk_metrics.summary}</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Model Quality */}
|
||||||
|
<Card>
|
||||||
|
<h2 className="text-sm font-medium text-gray-300 mb-3">Model Quality</h2>
|
||||||
|
{report.model_quality.windows.length > 0 ? (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-left text-xs text-gray-400 border-b border-surface-700">
|
||||||
|
<th className="pb-2 pr-4">Window</th>
|
||||||
|
<th className="pb-2 pr-4">Win Rate</th>
|
||||||
|
<th className="pb-2 pr-4">Dir. Accuracy</th>
|
||||||
|
<th className="pb-2 pr-4">IC</th>
|
||||||
|
<th className="pb-2 pr-4">ECE</th>
|
||||||
|
<th className="pb-2">Brier</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{report.model_quality.windows.map((w, i) => (
|
||||||
|
<tr key={i} className="border-b border-surface-800 text-gray-200">
|
||||||
|
<td className="py-1.5 pr-4 font-medium">{w.lookback}</td>
|
||||||
|
<td className="py-1.5 pr-4">{w.win_rate != null ? pct(w.win_rate) : '—'}</td>
|
||||||
|
<td className="py-1.5 pr-4">{w.directional_accuracy != null ? pct(w.directional_accuracy) : '—'}</td>
|
||||||
|
<td className="py-1.5 pr-4">{w.information_coefficient != null ? w.information_coefficient.toFixed(4) : '—'}</td>
|
||||||
|
<td className="py-1.5 pr-4">{w.calibration_error != null ? w.calibration_error.toFixed(4) : '—'}</td>
|
||||||
|
<td className="py-1.5">{w.brier_score != null ? w.brier_score.toFixed(4) : '—'}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-500">No model quality data available.</p>
|
||||||
|
)}
|
||||||
|
{report.model_quality.summary && (
|
||||||
|
<p className="text-xs text-gray-400 mt-3">{report.model_quality.summary}</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
|
import { useReports } from '../api/hooks';
|
||||||
|
import { DataTable, type Column } from '../components/DataTable';
|
||||||
|
import { StatusBadge, LoadingSpinner } from '../components/ui';
|
||||||
|
import type { ReportListItem } from '../api/hooks';
|
||||||
|
|
||||||
|
export function ReportsPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [reportType, setReportType] = useState('');
|
||||||
|
const { data, isLoading } = useReports({
|
||||||
|
report_type: reportType || undefined,
|
||||||
|
limit: 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns: Column<ReportListItem>[] = [
|
||||||
|
{
|
||||||
|
key: 'report_type',
|
||||||
|
header: 'Type',
|
||||||
|
render: (r) => (
|
||||||
|
<span className="inline-flex items-center rounded px-2 py-0.5 text-xs font-medium bg-surface-700 text-brand-300 capitalize">
|
||||||
|
{r.report_type}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'period_start',
|
||||||
|
header: 'Period',
|
||||||
|
render: (r) =>
|
||||||
|
r.period_start === r.period_end
|
||||||
|
? r.period_start
|
||||||
|
: `${r.period_start} → ${r.period_end}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'validation_status',
|
||||||
|
header: 'Validation',
|
||||||
|
render: (r) => <StatusBadge status={r.validation_status} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'generated_at',
|
||||||
|
header: 'Generated',
|
||||||
|
render: (r) => (
|
||||||
|
<span className="text-xs text-gray-400">
|
||||||
|
{new Date(r.generated_at).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isLoading) return <LoadingSpinner />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-semibold text-gray-100">
|
||||||
|
Trading Reports
|
||||||
|
</h1>
|
||||||
|
<select
|
||||||
|
value={reportType}
|
||||||
|
onChange={(e) => setReportType(e.target.value)}
|
||||||
|
className="rounded border border-surface-600 bg-surface-800 px-3 py-1.5 text-sm text-gray-200 focus:border-brand-500 focus:outline-none"
|
||||||
|
aria-label="Filter by report type"
|
||||||
|
>
|
||||||
|
<option value="">All Types</option>
|
||||||
|
<option value="daily">Daily</option>
|
||||||
|
<option value="weekly">Weekly</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<DataTable<ReportListItem>
|
||||||
|
data={data ?? []}
|
||||||
|
columns={columns}
|
||||||
|
keyField="id"
|
||||||
|
onRowClick={(row) =>
|
||||||
|
navigate({ to: '/reports/$id', params: { id: row.id } })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -30,6 +30,8 @@ import { HomePage } from './pages/Home';
|
|||||||
import { GlobalEventsPage } from './pages/GlobalEvents';
|
import { GlobalEventsPage } from './pages/GlobalEvents';
|
||||||
import { GlobalEventDetailPage } from './pages/GlobalEventDetail';
|
import { GlobalEventDetailPage } from './pages/GlobalEventDetail';
|
||||||
import { AgentsPage } from './pages/Agents';
|
import { AgentsPage } from './pages/Agents';
|
||||||
|
import { ReportsPage } from './pages/Reports';
|
||||||
|
import { ReportDetailPage } from './pages/ReportDetail';
|
||||||
|
|
||||||
// Root route wraps everything in the app shell layout
|
// Root route wraps everything in the app shell layout
|
||||||
const rootRoute = createRootRoute({
|
const rootRoute = createRootRoute({
|
||||||
@@ -167,6 +169,17 @@ const agentsRoute = createRoute({
|
|||||||
component: AgentsPage,
|
component: AgentsPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const reportsRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: '/reports',
|
||||||
|
component: ReportsPage,
|
||||||
|
});
|
||||||
|
const reportDetailRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: '/reports/$id',
|
||||||
|
component: ReportDetailPage,
|
||||||
|
});
|
||||||
|
|
||||||
const routeTree = rootRoute.addChildren([
|
const routeTree = rootRoute.addChildren([
|
||||||
indexRoute,
|
indexRoute,
|
||||||
companiesRoute,
|
companiesRoute,
|
||||||
@@ -192,6 +205,8 @@ const routeTree = rootRoute.addChildren([
|
|||||||
globalEventsRoute,
|
globalEventsRoute,
|
||||||
globalEventDetailRoute,
|
globalEventDetailRoute,
|
||||||
agentsRoute,
|
agentsRoute,
|
||||||
|
reportsRoute,
|
||||||
|
reportDetailRoute,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const router = createRouter({ routeTree });
|
export const router = createRouter({ routeTree });
|
||||||
|
|||||||
Reference in New Issue
Block a user