Files
pn-new-crm/src/components/dashboard/pipeline-funnel-chart.tsx

86 lines
3.0 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 { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import { CardSkeleton } from '@/components/shared/loading-skeleton';
import { EmptyState } from '@/components/shared/empty-state';
fix(ux): batch UX audit fixes across spine pages Comprehensive audit findings rolled up into one pass. Bugs: - dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] / sm:top-[50%]) were being silently stripped by tailwind-merge because the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced with explicit per-side utilities (top-0 right-0 bottom-0 left-0 + sm:right-auto sm:bottom-auto). Every Dialog instance now centers correctly on desktop. (Affected 16 dialog consumers.) - interest-documents-tab.tsx — useQuery shared the queryKey ['interests', interestId] with the parent InterestDetail's query but returned a different shape ({ data: ... } envelope vs unwrapped). They clobbered each other's cache on tab mount, degenerating the parent header to "Unknown Client" / "Open" briefly. Unified the queryFn shape so the cache stays consistent. - interest-tabs.tsx — milestone steps now derive done-state from PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as well as from the date stamp. Stage truth > date truth. Seeded / imported interests that arrived past `open` without per-step dates now correctly show their milestone steps as checked. - interest-detail.tsx — wires useMobileChrome so the mobile topbar shows the client name instead of the interest UUID. - interest-documents-tab.tsx — empty state restructured to a centered "No documents yet — Generate EOI" CTA card instead of a small primary button floating in the corner. - timeline/route.ts — synthesizes a "Created at <stage>" event when no audit-log rows exist for the interest, so the Activity tab isn't empty for seeded interests. - lead-source-chart.tsx — pie radii switched from fixed 90px/50px to "70%"/"40%" so the pie scales with the container instead of being clipped at narrow widths; reserved 40px for the legend. Visual / clarity: - interest-detail-header.tsx — Won/Lost rendered as branded text buttons on desktop ("Mark won", "Close as lost") and icon-only on mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen promoted to a labeled button when the interest is closed. Added "Last contact Xd ago" to the meta row. - detail-header-strip.tsx — py-4 → py-3 (tighter strip). - interest-tabs.tsx — milestone cards: the next pending milestone gets a brand-blue ring + "NEXT" pill so the user can see at a glance which lifecycle to act on. Its primary action gets the filled button variant. - interest-tabs.tsx — Deposit milestone: invoice flow promoted to primary CTA ("Create deposit invoice"), manual stage advance demoted to a small text link ("Mark received manually"). Reflects the actual recommended path now that recordPayment auto-advances on payment. - inline-editable-field.tsx — pencil affordance shown faintly (opacity-20) at rest so users discover that fields are editable without having to hover-test every label. Lifts to opacity-60 on hover. - constants.ts — STAGE_SHORT_LABELS map for cramped contexts; pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile via useIsMobile, so the rotated 9-stage axis isn't a wall of overlap on a 393px screen. - client-pipeline-summary.tsx — StageStepper rebuilt as a single segmented progress bar instead of 9 micro-dots + connectors that rendered inconsistently at tight widths. Each stage is an equal slice that lights up as the interest reaches it; tooltips on hover give the full stage name. Also dropped a pre-existing dead `br` variable. - dashboard empty states — Lead Source, Revenue Breakdown, Pipeline Funnel, and Recent Activity now have helpful descriptions explaining what populates them, instead of bare "No interests in range". - use-paginated-query.ts — reuses `&` when the endpoint already has `?`, so callers like the documents hub don't generate `…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API rejected as 400). Caught while testing the now-removed EOI route but applies broadly. tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1 pre-existing) on every file touched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
import { useIsMobile } from '@/hooks/use-is-mobile';
import { STAGE_SHORT_LABELS, safeStage, stageLabel } from '@/lib/constants';
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
import { ChartCard } from './chart-card';
import { useFunnel } from './use-analytics';
import type { DateRange } from '@/lib/services/analytics.service';
interface Props {
range: DateRange;
}
export function PipelineFunnelChart({ range }: Props) {
const { data, isLoading } = useFunnel(range);
fix(ux): batch UX audit fixes across spine pages Comprehensive audit findings rolled up into one pass. Bugs: - dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] / sm:top-[50%]) were being silently stripped by tailwind-merge because the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced with explicit per-side utilities (top-0 right-0 bottom-0 left-0 + sm:right-auto sm:bottom-auto). Every Dialog instance now centers correctly on desktop. (Affected 16 dialog consumers.) - interest-documents-tab.tsx — useQuery shared the queryKey ['interests', interestId] with the parent InterestDetail's query but returned a different shape ({ data: ... } envelope vs unwrapped). They clobbered each other's cache on tab mount, degenerating the parent header to "Unknown Client" / "Open" briefly. Unified the queryFn shape so the cache stays consistent. - interest-tabs.tsx — milestone steps now derive done-state from PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as well as from the date stamp. Stage truth > date truth. Seeded / imported interests that arrived past `open` without per-step dates now correctly show their milestone steps as checked. - interest-detail.tsx — wires useMobileChrome so the mobile topbar shows the client name instead of the interest UUID. - interest-documents-tab.tsx — empty state restructured to a centered "No documents yet — Generate EOI" CTA card instead of a small primary button floating in the corner. - timeline/route.ts — synthesizes a "Created at <stage>" event when no audit-log rows exist for the interest, so the Activity tab isn't empty for seeded interests. - lead-source-chart.tsx — pie radii switched from fixed 90px/50px to "70%"/"40%" so the pie scales with the container instead of being clipped at narrow widths; reserved 40px for the legend. Visual / clarity: - interest-detail-header.tsx — Won/Lost rendered as branded text buttons on desktop ("Mark won", "Close as lost") and icon-only on mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen promoted to a labeled button when the interest is closed. Added "Last contact Xd ago" to the meta row. - detail-header-strip.tsx — py-4 → py-3 (tighter strip). - interest-tabs.tsx — milestone cards: the next pending milestone gets a brand-blue ring + "NEXT" pill so the user can see at a glance which lifecycle to act on. Its primary action gets the filled button variant. - interest-tabs.tsx — Deposit milestone: invoice flow promoted to primary CTA ("Create deposit invoice"), manual stage advance demoted to a small text link ("Mark received manually"). Reflects the actual recommended path now that recordPayment auto-advances on payment. - inline-editable-field.tsx — pencil affordance shown faintly (opacity-20) at rest so users discover that fields are editable without having to hover-test every label. Lifts to opacity-60 on hover. - constants.ts — STAGE_SHORT_LABELS map for cramped contexts; pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile via useIsMobile, so the rotated 9-stage axis isn't a wall of overlap on a 393px screen. - client-pipeline-summary.tsx — StageStepper rebuilt as a single segmented progress bar instead of 9 micro-dots + connectors that rendered inconsistently at tight widths. Each stage is an equal slice that lights up as the interest reaches it; tooltips on hover give the full stage name. Also dropped a pre-existing dead `br` variable. - dashboard empty states — Lead Source, Revenue Breakdown, Pipeline Funnel, and Recent Activity now have helpful descriptions explaining what populates them, instead of bare "No interests in range". - use-paginated-query.ts — reuses `&` when the endpoint already has `?`, so callers like the documents hub don't generate `…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API rejected as 400). Caught while testing the now-removed EOI route but applies broadly. tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1 pre-existing) on every file touched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
const isMobile = useIsMobile();
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
const stages = data?.stages ?? [];
fix(ux): batch UX audit fixes across spine pages Comprehensive audit findings rolled up into one pass. Bugs: - dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] / sm:top-[50%]) were being silently stripped by tailwind-merge because the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced with explicit per-side utilities (top-0 right-0 bottom-0 left-0 + sm:right-auto sm:bottom-auto). Every Dialog instance now centers correctly on desktop. (Affected 16 dialog consumers.) - interest-documents-tab.tsx — useQuery shared the queryKey ['interests', interestId] with the parent InterestDetail's query but returned a different shape ({ data: ... } envelope vs unwrapped). They clobbered each other's cache on tab mount, degenerating the parent header to "Unknown Client" / "Open" briefly. Unified the queryFn shape so the cache stays consistent. - interest-tabs.tsx — milestone steps now derive done-state from PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as well as from the date stamp. Stage truth > date truth. Seeded / imported interests that arrived past `open` without per-step dates now correctly show their milestone steps as checked. - interest-detail.tsx — wires useMobileChrome so the mobile topbar shows the client name instead of the interest UUID. - interest-documents-tab.tsx — empty state restructured to a centered "No documents yet — Generate EOI" CTA card instead of a small primary button floating in the corner. - timeline/route.ts — synthesizes a "Created at <stage>" event when no audit-log rows exist for the interest, so the Activity tab isn't empty for seeded interests. - lead-source-chart.tsx — pie radii switched from fixed 90px/50px to "70%"/"40%" so the pie scales with the container instead of being clipped at narrow widths; reserved 40px for the legend. Visual / clarity: - interest-detail-header.tsx — Won/Lost rendered as branded text buttons on desktop ("Mark won", "Close as lost") and icon-only on mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen promoted to a labeled button when the interest is closed. Added "Last contact Xd ago" to the meta row. - detail-header-strip.tsx — py-4 → py-3 (tighter strip). - interest-tabs.tsx — milestone cards: the next pending milestone gets a brand-blue ring + "NEXT" pill so the user can see at a glance which lifecycle to act on. Its primary action gets the filled button variant. - interest-tabs.tsx — Deposit milestone: invoice flow promoted to primary CTA ("Create deposit invoice"), manual stage advance demoted to a small text link ("Mark received manually"). Reflects the actual recommended path now that recordPayment auto-advances on payment. - inline-editable-field.tsx — pencil affordance shown faintly (opacity-20) at rest so users discover that fields are editable without having to hover-test every label. Lifts to opacity-60 on hover. - constants.ts — STAGE_SHORT_LABELS map for cramped contexts; pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile via useIsMobile, so the rotated 9-stage axis isn't a wall of overlap on a 393px screen. - client-pipeline-summary.tsx — StageStepper rebuilt as a single segmented progress bar instead of 9 micro-dots + connectors that rendered inconsistently at tight widths. Each stage is an equal slice that lights up as the interest reaches it; tooltips on hover give the full stage name. Also dropped a pre-existing dead `br` variable. - dashboard empty states — Lead Source, Revenue Breakdown, Pipeline Funnel, and Recent Activity now have helpful descriptions explaining what populates them, instead of bare "No interests in range". - use-paginated-query.ts — reuses `&` when the endpoint already has `?`, so callers like the documents hub don't generate `…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API rejected as 400). Caught while testing the now-removed EOI route but applies broadly. tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1 pre-existing) on every file touched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
// Use short labels on mobile so the rotated axis isn't a wall of overlap.
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
const chartData = stages.map((s) => ({
fix(ux): batch UX audit fixes across spine pages Comprehensive audit findings rolled up into one pass. Bugs: - dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] / sm:top-[50%]) were being silently stripped by tailwind-merge because the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced with explicit per-side utilities (top-0 right-0 bottom-0 left-0 + sm:right-auto sm:bottom-auto). Every Dialog instance now centers correctly on desktop. (Affected 16 dialog consumers.) - interest-documents-tab.tsx — useQuery shared the queryKey ['interests', interestId] with the parent InterestDetail's query but returned a different shape ({ data: ... } envelope vs unwrapped). They clobbered each other's cache on tab mount, degenerating the parent header to "Unknown Client" / "Open" briefly. Unified the queryFn shape so the cache stays consistent. - interest-tabs.tsx — milestone steps now derive done-state from PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as well as from the date stamp. Stage truth > date truth. Seeded / imported interests that arrived past `open` without per-step dates now correctly show their milestone steps as checked. - interest-detail.tsx — wires useMobileChrome so the mobile topbar shows the client name instead of the interest UUID. - interest-documents-tab.tsx — empty state restructured to a centered "No documents yet — Generate EOI" CTA card instead of a small primary button floating in the corner. - timeline/route.ts — synthesizes a "Created at <stage>" event when no audit-log rows exist for the interest, so the Activity tab isn't empty for seeded interests. - lead-source-chart.tsx — pie radii switched from fixed 90px/50px to "70%"/"40%" so the pie scales with the container instead of being clipped at narrow widths; reserved 40px for the legend. Visual / clarity: - interest-detail-header.tsx — Won/Lost rendered as branded text buttons on desktop ("Mark won", "Close as lost") and icon-only on mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen promoted to a labeled button when the interest is closed. Added "Last contact Xd ago" to the meta row. - detail-header-strip.tsx — py-4 → py-3 (tighter strip). - interest-tabs.tsx — milestone cards: the next pending milestone gets a brand-blue ring + "NEXT" pill so the user can see at a glance which lifecycle to act on. Its primary action gets the filled button variant. - interest-tabs.tsx — Deposit milestone: invoice flow promoted to primary CTA ("Create deposit invoice"), manual stage advance demoted to a small text link ("Mark received manually"). Reflects the actual recommended path now that recordPayment auto-advances on payment. - inline-editable-field.tsx — pencil affordance shown faintly (opacity-20) at rest so users discover that fields are editable without having to hover-test every label. Lifts to opacity-60 on hover. - constants.ts — STAGE_SHORT_LABELS map for cramped contexts; pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile via useIsMobile, so the rotated 9-stage axis isn't a wall of overlap on a 393px screen. - client-pipeline-summary.tsx — StageStepper rebuilt as a single segmented progress bar instead of 9 micro-dots + connectors that rendered inconsistently at tight widths. Each stage is an equal slice that lights up as the interest reaches it; tooltips on hover give the full stage name. Also dropped a pre-existing dead `br` variable. - dashboard empty states — Lead Source, Revenue Breakdown, Pipeline Funnel, and Recent Activity now have helpful descriptions explaining what populates them, instead of bare "No interests in range". - use-paginated-query.ts — reuses `&` when the endpoint already has `?`, so callers like the documents hub don't generate `…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API rejected as 400). Caught while testing the now-removed EOI route but applies broadly. tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1 pre-existing) on every file touched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
stage: isMobile ? STAGE_SHORT_LABELS[safeStage(s.stage)] : stageLabel(s.stage),
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
count: s.count,
conversionPct: s.conversionPct,
}));
const allZero = stages.every((s) => s.count === 0);
function toCsv(): string | null {
if (!stages.length) return null;
const header = 'stage,count,conversion_pct';
const rows = stages.map((s) => `${s.stage},${s.count},${s.conversionPct}`);
return [header, ...rows].join('\n');
}
return (
<ChartCard
title="Pipeline Funnel"
description="Interests by stage with conversion rate vs. open"
exportFilename={`pipeline-funnel-${range}`}
toCsv={toCsv}
>
{isLoading ? (
<CardSkeleton />
) : allZero ? (
fix(ux): batch UX audit fixes across spine pages Comprehensive audit findings rolled up into one pass. Bugs: - dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] / sm:top-[50%]) were being silently stripped by tailwind-merge because the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced with explicit per-side utilities (top-0 right-0 bottom-0 left-0 + sm:right-auto sm:bottom-auto). Every Dialog instance now centers correctly on desktop. (Affected 16 dialog consumers.) - interest-documents-tab.tsx — useQuery shared the queryKey ['interests', interestId] with the parent InterestDetail's query but returned a different shape ({ data: ... } envelope vs unwrapped). They clobbered each other's cache on tab mount, degenerating the parent header to "Unknown Client" / "Open" briefly. Unified the queryFn shape so the cache stays consistent. - interest-tabs.tsx — milestone steps now derive done-state from PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as well as from the date stamp. Stage truth > date truth. Seeded / imported interests that arrived past `open` without per-step dates now correctly show their milestone steps as checked. - interest-detail.tsx — wires useMobileChrome so the mobile topbar shows the client name instead of the interest UUID. - interest-documents-tab.tsx — empty state restructured to a centered "No documents yet — Generate EOI" CTA card instead of a small primary button floating in the corner. - timeline/route.ts — synthesizes a "Created at <stage>" event when no audit-log rows exist for the interest, so the Activity tab isn't empty for seeded interests. - lead-source-chart.tsx — pie radii switched from fixed 90px/50px to "70%"/"40%" so the pie scales with the container instead of being clipped at narrow widths; reserved 40px for the legend. Visual / clarity: - interest-detail-header.tsx — Won/Lost rendered as branded text buttons on desktop ("Mark won", "Close as lost") and icon-only on mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen promoted to a labeled button when the interest is closed. Added "Last contact Xd ago" to the meta row. - detail-header-strip.tsx — py-4 → py-3 (tighter strip). - interest-tabs.tsx — milestone cards: the next pending milestone gets a brand-blue ring + "NEXT" pill so the user can see at a glance which lifecycle to act on. Its primary action gets the filled button variant. - interest-tabs.tsx — Deposit milestone: invoice flow promoted to primary CTA ("Create deposit invoice"), manual stage advance demoted to a small text link ("Mark received manually"). Reflects the actual recommended path now that recordPayment auto-advances on payment. - inline-editable-field.tsx — pencil affordance shown faintly (opacity-20) at rest so users discover that fields are editable without having to hover-test every label. Lifts to opacity-60 on hover. - constants.ts — STAGE_SHORT_LABELS map for cramped contexts; pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile via useIsMobile, so the rotated 9-stage axis isn't a wall of overlap on a 393px screen. - client-pipeline-summary.tsx — StageStepper rebuilt as a single segmented progress bar instead of 9 micro-dots + connectors that rendered inconsistently at tight widths. Each stage is an equal slice that lights up as the interest reaches it; tooltips on hover give the full stage name. Also dropped a pre-existing dead `br` variable. - dashboard empty states — Lead Source, Revenue Breakdown, Pipeline Funnel, and Recent Activity now have helpful descriptions explaining what populates them, instead of bare "No interests in range". - use-paginated-query.ts — reuses `&` when the endpoint already has `?`, so callers like the documents hub don't generate `…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API rejected as 400). Caught while testing the now-removed EOI route but applies broadly. tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1 pre-existing) on every file touched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
<EmptyState
title="No interests in range"
description="Conversion through Open → EOI → Deposit → Contract appears here. Try a longer date range, or add an interest to see it."
/>
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
) : (
<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,
}}
formatter={(value, _name, item) => {
const pct = (item?.payload as { conversionPct?: number } | undefined)
?.conversionPct;
return [`${value} (${pct ?? 0}%)`, 'Count'];
}}
/>
<Bar dataKey="count" fill="hsl(var(--chart-1))" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
)}
</ChartCard>
);
}