Files
pn-new-crm/src/components/dashboard/source-conversion-chart.tsx
Matt 221ae5784e chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:

- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
  never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
  after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
  redirects (ocr to ai, reports to dashboard, invitations to users),
  docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
  flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
  let-reassign), set-state-in-effect disables in CountryFlag and
  UploadForSigning preview-bytes effect, unused 'confirm' destructures in
  interest contract + reservation tabs, unescaped apostrophe in test-template
  card copy
2026-05-23 00:52:59 +02:00

91 lines
3.2 KiB
TypeScript

'use client';
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/lib/api/client';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { formatEnum } from '@/lib/constants';
interface SourceRow {
source: string;
total: number;
won: number;
lost: number;
conversionRate: number;
}
interface SourceConversionResponse {
data: SourceRow[];
}
/**
* Horizontal bar list of lead-source conversion rates. Complements the
* existing Lead Source Attribution donut: that one shows where leads
* COME from, this shows which sources actually CONVERT. Lets marketing
* spend follow the buyers, not the tire-kickers.
*
* Renders only sources with at least one lead; uses a compact bar-in-
* row layout so 5-8 sources fit comfortably without scrolling.
*/
export function SourceConversionChart() {
const { data, isLoading } = useQuery<SourceConversionResponse>({
queryKey: ['dashboard', 'source_conversion'],
queryFn: () => apiFetch<SourceConversionResponse>('/api/v1/dashboard/source-conversion'),
staleTime: 60_000,
});
const rows = data?.data ?? [];
return (
<Card>
<CardHeader>
<CardTitle className="text-base">Source conversion</CardTitle>
<CardDescription>Won deals as a percentage of leads per source.</CardDescription>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-3">
<Skeleton className="h-8 w-full" aria-hidden />
<Skeleton className="h-8 w-full" aria-hidden />
<Skeleton className="h-8 w-full" aria-hidden />
</div>
) : rows.length === 0 ? (
<p className="py-4 text-center text-sm text-muted-foreground">
Once interests have a source assigned, conversion rates will appear here.
</p>
) : (
<ul className="space-y-3">
{rows.map((r) => {
const pct = Math.round(r.conversionRate * 100);
const label = formatEnum(r.source);
return (
<li key={r.source} className="space-y-1">
<div className="flex items-center justify-between text-xs">
<span className="font-medium text-foreground">{label}</span>
<span className="tabular-nums text-muted-foreground">
<span className="font-semibold text-foreground">{pct}%</span>
<span className="ml-1.5">
({r.won} won · {r.total} total)
</span>
</span>
</div>
{/* Inline bar - keeps the widget compact and lets eight
rows share the same vertical space a Recharts plot
would use for two. */}
<div className="h-2 w-full overflow-hidden rounded-full bg-muted">
<div
className="h-full rounded-full bg-primary transition-all"
style={{ width: `${Math.max(pct, 2)}%` }}
/>
</div>
</li>
);
})}
</ul>
)}
</CardContent>
</Card>
);
}