Files
pn-new-crm/src/components/dashboard/lead-source-chart.tsx

90 lines
2.4 KiB
TypeScript
Raw Normal View History

feat(phase-b): ship analytics dashboard, alerts, scanner PWA, dedup, audit view Phase B (Insights & Alerts) PR4-11 in one drop. Builds on the schema + service skeletons committed in PRs 1-3. PR4 Analytics dashboard — 4 chart types (funnel/timeline/breakdown/source), date-range picker (today/7d/30d/90d), CSV+PNG export per card. PR5 Alert rail UI + /alerts page — topbar bell w/ live count, dashboard right-rail, three-tab page (active/dismissed/resolved), socket-driven invalidation. Bell lazy-loads list on popover open to keep cold pages fast in non-dashboard routes. PR6 EOI queue tab on documents hub — filters to in-flight EOIs, count surfaces in tab label. PR7 Interests-by-berth tab on berth detail — replaces the stub. PR8 Expense duplicate detection — BullMQ job runs scan on create, yellow banner on detail w/ Merge / Not-a-duplicate, transactional merge consolidates receipts and archives the source. PR9 Receipt scanner PWA + multi-provider AI — port-scoped /scan route in its own (scanner) group with no dashboard chrome, dynamic per-port manifest, OpenAI + Claude provider abstraction, admin OCR settings page (port-level + super-admin global default w/ opt-in fallback), test-connection endpoint, manual-entry fallback when no key is configured. Verify form always shown before save — no ghost rows. PR10 Audit log read view — swap to tsvector full-text search on the existing GIN index, cursor pagination, filters for entity/action/user /date range, batched actor-email resolution. PR11 Real-API tests — opt-in receipt-ocr.spec (admin save+test, optional real-receipt parse via REALAPI_RECEIPT_FIXTURE) and alert-engine socket-fanout spec gated behind RUN_ALERT_ENGINE_REALAPI. Both skip cleanly without their gate envs so CI stays green. Test totals: vitest 690 -> 713, smoke 130 -> 138, realapi +2 opt-in. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:21:55 +02:00
'use client';
import { Cell, Legend, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts';
import { CardSkeleton } from '@/components/shared/loading-skeleton';
import { EmptyState } from '@/components/shared/empty-state';
import { ChartCard } from './chart-card';
import { useLeadSource } from './use-analytics';
import type { DateRange } from '@/lib/services/analytics.service';
interface Props {
range: DateRange;
}
const COLORS = [
'hsl(var(--chart-1))',
'hsl(var(--chart-2))',
'hsl(var(--chart-3))',
'hsl(var(--chart-4))',
'hsl(var(--chart-5))',
];
const SOURCE_LABELS: Record<string, string> = {
website: 'Website',
referral: 'Referral',
manual: 'Manual',
social: 'Social',
unspecified: 'Unspecified',
};
export function LeadSourceChart({ range }: Props) {
const { data, isLoading } = useLeadSource(range);
const slices = data?.slices ?? [];
function toCsv(): string | null {
if (!slices.length) return null;
const header = 'source,count';
const rows = slices.map((s) => `${s.source},${s.count}`);
return [header, ...rows].join('\n');
}
const chartData = slices.map((s) => ({
name: SOURCE_LABELS[s.source] ?? s.source,
value: s.count,
}));
return (
<ChartCard
title="Lead Source Attribution"
description="Where new interests came from"
exportFilename={`lead-source-${range}`}
toCsv={toCsv}
>
{isLoading ? (
<CardSkeleton />
) : !slices.length ? (
<EmptyState title="No interests in range" />
) : (
<ResponsiveContainer width="100%" height={260}>
<PieChart>
<Pie
data={chartData}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
outerRadius={90}
innerRadius={50}
paddingAngle={2}
>
{chartData.map((_, i) => (
<Cell key={i} fill={COLORS[i % COLORS.length]} />
))}
</Pie>
<Tooltip
contentStyle={{
background: 'hsl(var(--popover))',
border: '1px solid hsl(var(--border))',
borderRadius: '6px',
fontSize: 12,
}}
/>
<Legend wrapperStyle={{ fontSize: 12 }} />
</PieChart>
</ResponsiveContainer>
)}
</ChartCard>
);
}