95 lines
2.5 KiB
TypeScript
95 lines
2.5 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useQuery } from '@tanstack/react-query';
|
||
|
|
import {
|
||
|
|
Bar,
|
||
|
|
BarChart,
|
||
|
|
CartesianGrid,
|
||
|
|
ResponsiveContainer,
|
||
|
|
Tooltip,
|
||
|
|
XAxis,
|
||
|
|
YAxis,
|
||
|
|
} from 'recharts';
|
||
|
|
|
||
|
|
import { apiFetch } from '@/lib/api/client';
|
||
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
|
|
import { CardSkeleton } from '@/components/shared/loading-skeleton';
|
||
|
|
import { WidgetErrorBoundary } from './widget-error-boundary';
|
||
|
|
|
||
|
|
interface PipelineRow {
|
||
|
|
stage: string;
|
||
|
|
count: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
const STAGE_LABELS: Record<string, string> = {
|
||
|
|
open: 'Open',
|
||
|
|
details_sent: 'Details Sent',
|
||
|
|
in_communication: 'In Communication',
|
||
|
|
visited: 'Visited',
|
||
|
|
signed_eoi_nda: 'Signed EOI/NDA',
|
||
|
|
deposit_10pct: 'Deposit 10%',
|
||
|
|
contract: 'Contract',
|
||
|
|
completed: 'Completed',
|
||
|
|
};
|
||
|
|
|
||
|
|
function PipelineChartInner() {
|
||
|
|
const { data, isLoading } = useQuery<PipelineRow[]>({
|
||
|
|
queryKey: ['dashboard', 'pipeline'],
|
||
|
|
queryFn: () => apiFetch<PipelineRow[]>('/api/v1/dashboard/pipeline'),
|
||
|
|
staleTime: 60_000,
|
||
|
|
retry: 2,
|
||
|
|
});
|
||
|
|
|
||
|
|
if (isLoading) {
|
||
|
|
return <CardSkeleton />;
|
||
|
|
}
|
||
|
|
|
||
|
|
const chartData = (data ?? []).map((row) => ({
|
||
|
|
stage: STAGE_LABELS[row.stage] ?? row.stage,
|
||
|
|
count: row.count,
|
||
|
|
}));
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Card className="h-full">
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle className="text-base">Pipeline Overview</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<ResponsiveContainer width="100%" height={260}>
|
||
|
|
<BarChart data={chartData} margin={{ top: 4, right: 8, left: -16, bottom: 60 }}>
|
||
|
|
<CartesianGrid strokeDasharray="3 3" className="stroke-border" />
|
||
|
|
<XAxis
|
||
|
|
dataKey="stage"
|
||
|
|
tick={{ fontSize: 11, fill: 'hsl(var(--muted-foreground))' }}
|
||
|
|
angle={-40}
|
||
|
|
textAnchor="end"
|
||
|
|
interval={0}
|
||
|
|
/>
|
||
|
|
<YAxis
|
||
|
|
tick={{ fontSize: 11, fill: 'hsl(var(--muted-foreground))' }}
|
||
|
|
allowDecimals={false}
|
||
|
|
/>
|
||
|
|
<Tooltip
|
||
|
|
contentStyle={{
|
||
|
|
background: 'hsl(var(--popover))',
|
||
|
|
border: '1px solid hsl(var(--border))',
|
||
|
|
borderRadius: '6px',
|
||
|
|
fontSize: 12,
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
<Bar dataKey="count" fill="hsl(var(--chart-1))" radius={[4, 4, 0, 0]} />
|
||
|
|
</BarChart>
|
||
|
|
</ResponsiveContainer>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function PipelineChart() {
|
||
|
|
return (
|
||
|
|
<WidgetErrorBoundary>
|
||
|
|
<PipelineChartInner />
|
||
|
|
</WidgetErrorBoundary>
|
||
|
|
);
|
||
|
|
}
|