@handled-ai/design-system 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/dist/charts/bar-chart-component.d.ts +24 -0
- package/dist/charts/bar-chart-component.js +123 -0
- package/dist/charts/bar-chart-component.js.map +1 -0
- package/dist/charts/chart-tooltip.d.ts +26 -0
- package/dist/charts/chart-tooltip.js +69 -0
- package/dist/charts/chart-tooltip.js.map +1 -0
- package/dist/charts/chart.d.ts +64 -0
- package/dist/charts/chart.js +285 -0
- package/dist/charts/chart.js.map +1 -0
- package/dist/charts/donut-chart.d.ts +21 -0
- package/dist/charts/donut-chart.js +96 -0
- package/dist/charts/donut-chart.js.map +1 -0
- package/dist/charts/index.d.ts +11 -0
- package/dist/charts/index.js +10 -0
- package/dist/charts/index.js.map +1 -0
- package/dist/charts/pipeline-overview.d.ts +76 -0
- package/dist/charts/pipeline-overview.js +372 -0
- package/dist/charts/pipeline-overview.js.map +1 -0
- package/dist/charts/sankey-chart.d.ts +52 -0
- package/dist/charts/sankey-chart.js +219 -0
- package/dist/charts/sankey-chart.js.map +1 -0
- package/dist/charts/top-line-metrics.d.ts +26 -0
- package/dist/charts/top-line-metrics.js +224 -0
- package/dist/charts/top-line-metrics.js.map +1 -0
- package/dist/charts/trend-area-chart.d.ts +21 -0
- package/dist/charts/trend-area-chart.js +150 -0
- package/dist/charts/trend-area-chart.js.map +1 -0
- package/dist/charts/volume-analysis-chart.d.ts +19 -0
- package/dist/charts/volume-analysis-chart.js +121 -0
- package/dist/charts/volume-analysis-chart.js.map +1 -0
- package/dist/components/activity-detail.d.ts +38 -0
- package/dist/components/activity-detail.js +163 -0
- package/dist/components/activity-detail.js.map +1 -0
- package/dist/components/activity-log.d.ts +21 -0
- package/dist/components/activity-log.js +61 -0
- package/dist/components/activity-log.js.map +1 -0
- package/dist/components/agent-popover.d.ts +71 -0
- package/dist/components/agent-popover.js +282 -0
- package/dist/components/agent-popover.js.map +1 -0
- package/dist/components/agent-widget.d.ts +24 -0
- package/dist/components/agent-widget.js +117 -0
- package/dist/components/agent-widget.js.map +1 -0
- package/dist/components/avatar.d.ts +13 -0
- package/dist/components/avatar.js +140 -0
- package/dist/components/avatar.js.map +1 -0
- package/dist/components/badge.d.ts +12 -0
- package/dist/components/badge.js +75 -0
- package/dist/components/badge.js.map +1 -0
- package/dist/components/button.d.ts +13 -0
- package/dist/components/button.js +83 -0
- package/dist/components/button.js.map +1 -0
- package/dist/components/card.d.ts +11 -0
- package/dist/components/card.js +119 -0
- package/dist/components/card.js.map +1 -0
- package/dist/components/contact-list.d.ts +34 -0
- package/dist/components/contact-list.js +84 -0
- package/dist/components/contact-list.js.map +1 -0
- package/dist/components/dashboard-cards.d.ts +10 -0
- package/dist/components/dashboard-cards.js +164 -0
- package/dist/components/dashboard-cards.js.map +1 -0
- package/dist/components/data-table-display.d.ts +19 -0
- package/dist/components/data-table-display.js +109 -0
- package/dist/components/data-table-display.js.map +1 -0
- package/dist/components/data-table-filter.d.ts +18 -0
- package/dist/components/data-table-filter.js +107 -0
- package/dist/components/data-table-filter.js.map +1 -0
- package/dist/components/data-table-quick-views.d.ts +13 -0
- package/dist/components/data-table-quick-views.js +90 -0
- package/dist/components/data-table-quick-views.js.map +1 -0
- package/dist/components/data-table-toolbar.d.ts +18 -0
- package/dist/components/data-table-toolbar.js +45 -0
- package/dist/components/data-table-toolbar.js.map +1 -0
- package/dist/components/data-table.d.ts +39 -0
- package/dist/components/data-table.js +821 -0
- package/dist/components/data-table.js.map +1 -0
- package/dist/components/detail-view.d.ts +44 -0
- package/dist/components/detail-view.js +165 -0
- package/dist/components/detail-view.js.map +1 -0
- package/dist/components/dialog.d.ts +19 -0
- package/dist/components/dialog.js +188 -0
- package/dist/components/dialog.js.map +1 -0
- package/dist/components/dropdown-menu.d.ts +27 -0
- package/dist/components/dropdown-menu.js +279 -0
- package/dist/components/dropdown-menu.js.map +1 -0
- package/dist/components/entity-panel.d.ts +69 -0
- package/dist/components/entity-panel.js +584 -0
- package/dist/components/entity-panel.js.map +1 -0
- package/dist/components/inbox-row.d.ts +27 -0
- package/dist/components/inbox-row.js +139 -0
- package/dist/components/inbox-row.js.map +1 -0
- package/dist/components/inbox-toolbar.d.ts +21 -0
- package/dist/components/inbox-toolbar.js +203 -0
- package/dist/components/inbox-toolbar.js.map +1 -0
- package/dist/components/input.d.ts +5 -0
- package/dist/components/input.js +50 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/insights-filter-bar.d.ts +21 -0
- package/dist/components/insights-filter-bar.js +99 -0
- package/dist/components/insights-filter-bar.js.map +1 -0
- package/dist/components/item-list-display.d.ts +22 -0
- package/dist/components/item-list-display.js +240 -0
- package/dist/components/item-list-display.js.map +1 -0
- package/dist/components/item-list-filter.d.ts +16 -0
- package/dist/components/item-list-filter.js +87 -0
- package/dist/components/item-list-filter.js.map +1 -0
- package/dist/components/item-list-toolbar.d.ts +25 -0
- package/dist/components/item-list-toolbar.js +79 -0
- package/dist/components/item-list-toolbar.js.map +1 -0
- package/dist/components/item-list.d.ts +20 -0
- package/dist/components/item-list.js +702 -0
- package/dist/components/item-list.js.map +1 -0
- package/dist/components/label.d.ts +6 -0
- package/dist/components/label.js +55 -0
- package/dist/components/label.js.map +1 -0
- package/dist/components/message.d.ts +23 -0
- package/dist/components/message.js +117 -0
- package/dist/components/message.js.map +1 -0
- package/dist/components/metric-card.d.ts +25 -0
- package/dist/components/metric-card.js +107 -0
- package/dist/components/metric-card.js.map +1 -0
- package/dist/components/performance-metrics-table.d.ts +38 -0
- package/dist/components/performance-metrics-table.js +342 -0
- package/dist/components/performance-metrics-table.js.map +1 -0
- package/dist/components/preview-list.d.ts +14 -0
- package/dist/components/preview-list.js +83 -0
- package/dist/components/preview-list.js.map +1 -0
- package/dist/components/progress.d.ts +6 -0
- package/dist/components/progress.js +69 -0
- package/dist/components/progress.js.map +1 -0
- package/dist/components/quick-action-chat-area.d.ts +24 -0
- package/dist/components/quick-action-chat-area.js +178 -0
- package/dist/components/quick-action-chat-area.js.map +1 -0
- package/dist/components/quick-action-modal.d.ts +30 -0
- package/dist/components/quick-action-modal.js +288 -0
- package/dist/components/quick-action-modal.js.map +1 -0
- package/dist/components/quick-action-sidebar-nav.d.ts +51 -0
- package/dist/components/quick-action-sidebar-nav.js +528 -0
- package/dist/components/quick-action-sidebar-nav.js.map +1 -0
- package/dist/components/recommended-actions-section.d.ts +23 -0
- package/dist/components/recommended-actions-section.js +215 -0
- package/dist/components/recommended-actions-section.js.map +1 -0
- package/dist/components/report-card.d.ts +26 -0
- package/dist/components/report-card.js +69 -0
- package/dist/components/report-card.js.map +1 -0
- package/dist/components/score-analysis-modal.d.ts +26 -0
- package/dist/components/score-analysis-modal.js +141 -0
- package/dist/components/score-analysis-modal.js.map +1 -0
- package/dist/components/score-breakdown.d.ts +17 -0
- package/dist/components/score-breakdown.js +162 -0
- package/dist/components/score-breakdown.js.map +1 -0
- package/dist/components/score-feedback.d.ts +40 -0
- package/dist/components/score-feedback.js +209 -0
- package/dist/components/score-feedback.js.map +1 -0
- package/dist/components/score-ring.d.ts +14 -0
- package/dist/components/score-ring.js +79 -0
- package/dist/components/score-ring.js.map +1 -0
- package/dist/components/scroll-area.d.ts +7 -0
- package/dist/components/scroll-area.js +101 -0
- package/dist/components/scroll-area.js.map +1 -0
- package/dist/components/select.d.ts +17 -0
- package/dist/components/select.js +228 -0
- package/dist/components/select.js.map +1 -0
- package/dist/components/separator.d.ts +6 -0
- package/dist/components/separator.js +61 -0
- package/dist/components/separator.js.map +1 -0
- package/dist/components/sheet.d.ts +16 -0
- package/dist/components/sheet.js +168 -0
- package/dist/components/sheet.js.map +1 -0
- package/dist/components/sidebar.d.ts +73 -0
- package/dist/components/sidebar.js +723 -0
- package/dist/components/sidebar.js.map +1 -0
- package/dist/components/signal-feedback-inline.d.ts +51 -0
- package/dist/components/signal-feedback-inline.js +548 -0
- package/dist/components/signal-feedback-inline.js.map +1 -0
- package/dist/components/simple-data-table.d.ts +15 -0
- package/dist/components/simple-data-table.js +91 -0
- package/dist/components/simple-data-table.js.map +1 -0
- package/dist/components/skeleton.d.ts +5 -0
- package/dist/components/skeleton.js +44 -0
- package/dist/components/skeleton.js.map +1 -0
- package/dist/components/status-badge.d.ts +10 -0
- package/dist/components/status-badge.js +82 -0
- package/dist/components/status-badge.js.map +1 -0
- package/dist/components/styled-bar-list.d.ts +20 -0
- package/dist/components/styled-bar-list.js +59 -0
- package/dist/components/styled-bar-list.js.map +1 -0
- package/dist/components/suggested-actions.d.ts +110 -0
- package/dist/components/suggested-actions.js +1538 -0
- package/dist/components/suggested-actions.js.map +1 -0
- package/dist/components/table.d.ts +12 -0
- package/dist/components/table.js +147 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/tabs.d.ts +14 -0
- package/dist/components/tabs.js +129 -0
- package/dist/components/tabs.js.map +1 -0
- package/dist/components/textarea.d.ts +5 -0
- package/dist/components/textarea.js +47 -0
- package/dist/components/textarea.js.map +1 -0
- package/dist/components/timeline-activity.d.ts +34 -0
- package/dist/components/timeline-activity.js +181 -0
- package/dist/components/timeline-activity.js.map +1 -0
- package/dist/components/tooltip.d.ts +9 -0
- package/dist/components/tooltip.js +93 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/components/view-mode-toggle.d.ts +16 -0
- package/dist/components/view-mode-toggle.js +24 -0
- package/dist/components/view-mode-toggle.js.map +1 -0
- package/dist/hooks/use-mobile.d.ts +3 -0
- package/dist/hooks/use-mobile.js +21 -0
- package/dist/hooks/use-mobile.js.map +1 -0
- package/dist/index.d.ts +68 -1878
- package/dist/index.js +69 -10918
- package/dist/index.js.map +1 -1
- package/dist/lib/icons.d.ts +18 -0
- package/dist/lib/icons.js +21 -0
- package/dist/lib/icons.js.map +1 -0
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js +9 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/prototype/index.d.ts +20 -0
- package/dist/prototype/index.js +8 -0
- package/dist/prototype/index.js.map +1 -0
- package/dist/prototype/prototype-accounts-view.d.ts +22 -0
- package/dist/prototype/prototype-accounts-view.js +70 -0
- package/dist/prototype/prototype-accounts-view.js.map +1 -0
- package/dist/prototype/prototype-admin-view.d.ts +21 -0
- package/dist/prototype/prototype-admin-view.js +53 -0
- package/dist/prototype/prototype-admin-view.js.map +1 -0
- package/dist/prototype/prototype-config.d.ts +226 -0
- package/dist/prototype/prototype-config.js +1 -0
- package/dist/prototype/prototype-config.js.map +1 -0
- package/dist/prototype/prototype-inbox-view.d.ts +48 -0
- package/dist/prototype/prototype-inbox-view.js +701 -0
- package/dist/prototype/prototype-inbox-view.js.map +1 -0
- package/dist/prototype/prototype-insights-view.d.ts +23 -0
- package/dist/prototype/prototype-insights-view.js +335 -0
- package/dist/prototype/prototype-insights-view.js.map +1 -0
- package/dist/prototype/prototype-shell.d.ts +40 -0
- package/dist/prototype/prototype-shell.js +190 -0
- package/dist/prototype/prototype-shell.js.map +1 -0
- package/dist/prototype/prototype-work-queue-view.d.ts +8 -0
- package/dist/prototype/prototype-work-queue-view.js +17 -0
- package/dist/prototype/prototype-work-queue-view.js.map +1 -0
- package/dist/three/agent-orb.d.ts +39 -0
- package/dist/three/agent-orb.js +500 -0
- package/dist/three/agent-orb.js.map +1 -0
- package/dist/three/index.d.ts +2 -0
- package/dist/three/index.js +2 -0
- package/dist/three/index.js.map +1 -0
- package/package.json +98 -17
- package/src/charts/bar-chart-component.tsx +150 -0
- package/src/charts/chart-tooltip.tsx +86 -0
- package/src/charts/chart.tsx +371 -0
- package/src/charts/donut-chart.tsx +112 -0
- package/src/charts/index.ts +13 -0
- package/src/charts/pipeline-overview.tsx +476 -0
- package/src/charts/sankey-chart.tsx +290 -0
- package/src/charts/top-line-metrics.tsx +261 -0
- package/src/charts/trend-area-chart.tsx +150 -0
- package/src/charts/volume-analysis-chart.tsx +124 -0
- package/src/components/activity-detail.tsx +233 -0
- package/src/components/activity-log.tsx +89 -0
- package/src/components/agent-popover.tsx +373 -0
- package/src/components/agent-widget.tsx +163 -0
- package/src/components/avatar.tsx +109 -0
- package/src/components/badge.tsx +48 -0
- package/src/components/button.tsx +59 -0
- package/src/components/card.tsx +92 -0
- package/src/components/contact-list.tsx +121 -0
- package/src/components/dashboard-cards.tsx +170 -0
- package/src/components/data-table-display.tsx +139 -0
- package/src/components/data-table-filter.tsx +138 -0
- package/src/components/data-table-quick-views.tsx +103 -0
- package/src/components/data-table-toolbar.tsx +56 -0
- package/src/components/data-table.tsx +915 -0
- package/src/components/detail-view.tsx +237 -0
- package/src/components/dialog.tsx +158 -0
- package/src/components/dropdown-menu.tsx +257 -0
- package/src/components/entity-panel.tsx +767 -0
- package/src/components/inbox-row.tsx +132 -0
- package/src/components/inbox-toolbar.tsx +213 -0
- package/src/components/input.tsx +21 -0
- package/src/components/insights-filter-bar.tsx +132 -0
- package/src/components/item-list-display.tsx +278 -0
- package/src/components/item-list-filter.tsx +118 -0
- package/src/components/item-list-toolbar.tsx +97 -0
- package/src/components/item-list.tsx +843 -0
- package/src/components/label.tsx +24 -0
- package/src/components/message.tsx +83 -0
- package/src/components/metric-card.tsx +178 -0
- package/src/components/performance-metrics-table.tsx +442 -0
- package/src/components/preview-list.tsx +62 -0
- package/src/components/progress.tsx +31 -0
- package/src/components/quick-action-chat-area.tsx +156 -0
- package/src/components/quick-action-modal.tsx +331 -0
- package/src/components/quick-action-sidebar-nav.tsx +592 -0
- package/src/components/recommended-actions-section.tsx +258 -0
- package/src/components/report-card.tsx +106 -0
- package/src/components/score-analysis-modal.tsx +172 -0
- package/src/components/score-breakdown.tsx +179 -0
- package/src/components/score-feedback.tsx +288 -0
- package/src/components/score-ring.tsx +87 -0
- package/src/components/scroll-area.tsx +58 -0
- package/src/components/select.tsx +190 -0
- package/src/components/separator.tsx +28 -0
- package/src/components/sheet.tsx +143 -0
- package/src/components/sidebar.tsx +726 -0
- package/src/components/signal-feedback-inline.tsx +591 -0
- package/src/components/simple-data-table.tsx +124 -0
- package/src/components/skeleton.tsx +15 -0
- package/src/components/status-badge.tsx +63 -0
- package/src/components/styled-bar-list.tsx +70 -0
- package/src/components/suggested-actions.tsx +1985 -0
- package/src/components/table.tsx +116 -0
- package/src/components/tabs.tsx +91 -0
- package/src/components/textarea.tsx +18 -0
- package/src/components/timeline-activity.tsx +234 -0
- package/src/components/tooltip.tsx +57 -0
- package/src/components/view-mode-toggle.tsx +39 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/index.ts +77 -0
- package/src/lib/icons.ts +18 -0
- package/src/lib/utils.ts +6 -0
- package/src/prototype/index.ts +11 -0
- package/src/prototype/prototype-accounts-view.tsx +112 -0
- package/src/prototype/prototype-admin-view.tsx +67 -0
- package/src/prototype/prototype-config.ts +243 -0
- package/src/prototype/prototype-inbox-view.tsx +810 -0
- package/src/prototype/prototype-insights-view.tsx +379 -0
- package/src/prototype/prototype-shell.tsx +219 -0
- package/src/prototype/prototype-work-queue-view.tsx +30 -0
- package/src/styles/globals.css +299 -0
- package/src/three/agent-orb.tsx +557 -0
- package/src/three/index.ts +5 -0
- package/src/types/r3f.d.ts +8 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { TrendingUp, Info, ArrowRight } from "lucide-react"
|
|
5
|
+
import { ResponsiveSankey } from "@nivo/sankey"
|
|
6
|
+
|
|
7
|
+
import { cn } from "../lib/utils"
|
|
8
|
+
import {
|
|
9
|
+
Tooltip,
|
|
10
|
+
TooltipContent,
|
|
11
|
+
TooltipProvider,
|
|
12
|
+
TooltipTrigger,
|
|
13
|
+
} from "../components/tooltip"
|
|
14
|
+
|
|
15
|
+
export interface PipelineStage {
|
|
16
|
+
id: string
|
|
17
|
+
label: string
|
|
18
|
+
count: number
|
|
19
|
+
trend: string
|
|
20
|
+
nextConversion: string | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PipelineStageMetrics {
|
|
24
|
+
medianTime: string
|
|
25
|
+
avgTime: string
|
|
26
|
+
dropOffs: { reason: string; count: number; pct: string }[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PipelineStageTiming {
|
|
30
|
+
median: string
|
|
31
|
+
avg: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PipelineFilterBreakdown {
|
|
35
|
+
[stageId: string]: Record<string, number>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SEGMENT_PALETTE = [
|
|
39
|
+
"#0F4C3A",
|
|
40
|
+
"#15803d",
|
|
41
|
+
"#0ea5e9",
|
|
42
|
+
"#8b5cf6",
|
|
43
|
+
"#f59e0b",
|
|
44
|
+
"#ef4444",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
const DROP_OFF_NODES = [
|
|
48
|
+
{ id: "Lost/Other", nodeColor: "#CBD5E1" },
|
|
49
|
+
{ id: "Coverage", nodeColor: "#F59E0B" },
|
|
50
|
+
{ id: "Unqualified", nodeColor: "#F59E0B" },
|
|
51
|
+
{ id: "No Contact", nodeColor: "#F59E0B" },
|
|
52
|
+
{ id: "Intake Drop", nodeColor: "#F59E0B" },
|
|
53
|
+
{ id: "No Show/Cancel", nodeColor: "#F59E0B" },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
function StageHoverCard({
|
|
57
|
+
title,
|
|
58
|
+
count,
|
|
59
|
+
metrics,
|
|
60
|
+
}: {
|
|
61
|
+
title: string
|
|
62
|
+
count: number | string
|
|
63
|
+
metrics?: PipelineStageMetrics
|
|
64
|
+
}) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="w-[260px] overflow-hidden rounded-lg border border-border bg-card font-sans text-left shadow-xl">
|
|
67
|
+
<div className="border-b border-border p-3">
|
|
68
|
+
<div className="mb-0.5 text-xs font-medium text-muted-foreground">{title}</div>
|
|
69
|
+
<div className="text-2xl font-bold text-foreground">{count}</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{metrics && (
|
|
73
|
+
<div className="grid grid-cols-2 gap-2 border-b border-border bg-muted/30 px-3 py-2">
|
|
74
|
+
<div>
|
|
75
|
+
<div className="text-[9px] font-semibold uppercase tracking-wider text-muted-foreground/70">
|
|
76
|
+
Median
|
|
77
|
+
</div>
|
|
78
|
+
<div className="text-xs font-bold text-foreground">
|
|
79
|
+
{metrics.medianTime}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="border-l border-border pl-2">
|
|
83
|
+
<div className="text-[9px] font-semibold uppercase tracking-wider text-muted-foreground/70">
|
|
84
|
+
Average
|
|
85
|
+
</div>
|
|
86
|
+
<div className="text-xs font-bold text-foreground">
|
|
87
|
+
{metrics.avgTime}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{metrics?.dropOffs && metrics.dropOffs.length > 0 && (
|
|
94
|
+
<div className="p-2">
|
|
95
|
+
<div className="mb-1.5 px-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
96
|
+
Drop-off Reasons
|
|
97
|
+
</div>
|
|
98
|
+
<div className="space-y-0.5">
|
|
99
|
+
{metrics.dropOffs.map((drop, i) => (
|
|
100
|
+
<div
|
|
101
|
+
key={i}
|
|
102
|
+
className="group flex cursor-pointer items-center justify-between rounded p-1.5 text-xs transition-colors hover:bg-muted"
|
|
103
|
+
>
|
|
104
|
+
<span className="truncate pr-2 text-muted-foreground group-hover:text-foreground">
|
|
105
|
+
{drop.reason}
|
|
106
|
+
</span>
|
|
107
|
+
<div className="flex items-center gap-1.5">
|
|
108
|
+
<span className="text-[10px] text-muted-foreground/70">{drop.pct}</span>
|
|
109
|
+
<span className="inline-flex h-4 min-w-[20px] items-center justify-center rounded border border-border bg-muted px-1 text-[9px] font-semibold text-muted-foreground group-hover:bg-muted group-hover:text-foreground">
|
|
110
|
+
{drop.count}
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
<div className="flex cursor-pointer items-center justify-between border-t border-border bg-muted px-3 py-2 text-xs font-medium text-blue-600 transition-colors hover:bg-muted hover:text-blue-700">
|
|
120
|
+
View in Work Queue <ArrowRight className="h-3 w-3" />
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface PipelineOverviewProps {
|
|
127
|
+
title?: string
|
|
128
|
+
stages: PipelineStage[]
|
|
129
|
+
stageMetrics: Record<string, PipelineStageMetrics>
|
|
130
|
+
stageTimings: (PipelineStageTiming | null)[]
|
|
131
|
+
filterOptions?: string[]
|
|
132
|
+
onFilterChange?: (filterOption: string) => void
|
|
133
|
+
filterBreakdowns?: Record<string, PipelineFilterBreakdown>
|
|
134
|
+
countingModes?: string[]
|
|
135
|
+
countingModeTooltip?: string
|
|
136
|
+
/** Main pipeline flow nodes (after the first stage) */
|
|
137
|
+
flowNodes?: { id: string; nodeColor: string }[]
|
|
138
|
+
/** Drop-off distribution from initial stage: { "Lost/Other": 56, "Coverage": 40, ... } */
|
|
139
|
+
dropOffDistribution?: Record<string, number>
|
|
140
|
+
/** Flow links after the first stage (middle of pipeline onward) */
|
|
141
|
+
flowLinks?: { source: string; target: string; value: number }[]
|
|
142
|
+
totalReceived?: number
|
|
143
|
+
/** Dollar amounts to display on terminal node labels: { "Retained": "$18.2M" } */
|
|
144
|
+
nodeAmounts?: Record<string, string>
|
|
145
|
+
/** Unit noun for standard node/link tooltips (e.g. "signals", "accounts"). */
|
|
146
|
+
unitLabel?: string
|
|
147
|
+
/** Unit noun for terminal/drop-off node tooltips (e.g. "opportunities"). Defaults to unitLabel. */
|
|
148
|
+
terminalUnitLabel?: string
|
|
149
|
+
/** Node IDs that should use terminalUnitLabel. Defaults to keys of dropOffDistribution. */
|
|
150
|
+
terminalNodeIds?: string[]
|
|
151
|
+
/** Sankey chart margins, merged with defaults { top: 20, right: 120, bottom: 20, left: 140 }. */
|
|
152
|
+
sankeyMargin?: { top?: number; right?: number; bottom?: number; left?: number }
|
|
153
|
+
/** Gap between Sankey node bar and its outside label. */
|
|
154
|
+
sankeyLabelPadding?: number
|
|
155
|
+
/** When true, conversion badges use `flex`; when false, `hidden xl:flex`. */
|
|
156
|
+
alwaysShowConversionBadges?: boolean
|
|
157
|
+
/** Color for dynamically generated drop-off nodes. */
|
|
158
|
+
dropOffNodeColor?: string
|
|
159
|
+
onViewInWorkQueue?: (stageId: string) => void
|
|
160
|
+
className?: string
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function PipelineOverview({
|
|
164
|
+
title = "Pipeline Overview",
|
|
165
|
+
stages,
|
|
166
|
+
stageMetrics,
|
|
167
|
+
stageTimings,
|
|
168
|
+
filterOptions = ["Facility", "Source", "Lead Source", "Payer", "Channel"],
|
|
169
|
+
onFilterChange,
|
|
170
|
+
filterBreakdowns,
|
|
171
|
+
countingModes = ["Unique Patients", "All Referrals"],
|
|
172
|
+
countingModeTooltip = "Patients may be referred through multiple channels. 'All Referrals' shows total volume; 'Unique Patients' deduplicates to show distinct patient counts.",
|
|
173
|
+
flowNodes = [
|
|
174
|
+
{ id: "Contacted", nodeColor: "#2A8F7A" },
|
|
175
|
+
{ id: "Intake Sent", nodeColor: "#3DB4A0" },
|
|
176
|
+
{ id: "Intake Done", nodeColor: "#4CC9B0" },
|
|
177
|
+
{ id: "Scheduled", nodeColor: "#5FCFBC" },
|
|
178
|
+
{ id: "Completed", nodeColor: "#79E2C9" },
|
|
179
|
+
],
|
|
180
|
+
dropOffDistribution = {
|
|
181
|
+
"Lost/Other": 56,
|
|
182
|
+
Coverage: 40,
|
|
183
|
+
Unqualified: 30,
|
|
184
|
+
},
|
|
185
|
+
flowLinks = [
|
|
186
|
+
{ source: "Contacted", target: "Intake Sent", value: 660 },
|
|
187
|
+
{ source: "Contacted", target: "No Contact", value: 60 },
|
|
188
|
+
{ source: "Intake Sent", target: "Intake Done", value: 612 },
|
|
189
|
+
{ source: "Intake Sent", target: "Intake Drop", value: 48 },
|
|
190
|
+
{ source: "Intake Done", target: "Scheduled", value: 612 },
|
|
191
|
+
{ source: "Scheduled", target: "Completed", value: 520 },
|
|
192
|
+
{ source: "Scheduled", target: "No Show/Cancel", value: 92 },
|
|
193
|
+
],
|
|
194
|
+
totalReceived = 847,
|
|
195
|
+
nodeAmounts,
|
|
196
|
+
unitLabel,
|
|
197
|
+
terminalUnitLabel,
|
|
198
|
+
terminalNodeIds,
|
|
199
|
+
sankeyMargin,
|
|
200
|
+
sankeyLabelPadding,
|
|
201
|
+
alwaysShowConversionBadges = false,
|
|
202
|
+
dropOffNodeColor,
|
|
203
|
+
onViewInWorkQueue: _onViewInWorkQueue,
|
|
204
|
+
className,
|
|
205
|
+
}: PipelineOverviewProps) {
|
|
206
|
+
const [selectedFilter, setSelectedFilter] = React.useState(filterOptions[0])
|
|
207
|
+
const [countingMode, setCountingMode] = React.useState(countingModes[0])
|
|
208
|
+
|
|
209
|
+
const effectiveUnitLabel = unitLabel ?? "items"
|
|
210
|
+
const effectiveTerminalUnitLabel = terminalUnitLabel ?? effectiveUnitLabel
|
|
211
|
+
const effectiveTerminalNodeIds = React.useMemo(
|
|
212
|
+
() => new Set(terminalNodeIds ?? Object.keys(dropOffDistribution)),
|
|
213
|
+
[terminalNodeIds, dropOffDistribution],
|
|
214
|
+
)
|
|
215
|
+
const effectiveMargin = React.useMemo(
|
|
216
|
+
() => ({ top: 20, right: 120, bottom: 20, left: 140, ...sankeyMargin }),
|
|
217
|
+
[sankeyMargin],
|
|
218
|
+
)
|
|
219
|
+
const effectiveLabelPadding = sankeyLabelPadding ?? 16
|
|
220
|
+
|
|
221
|
+
const sankeyData = React.useMemo(() => {
|
|
222
|
+
const breakdown =
|
|
223
|
+
filterBreakdowns?.[selectedFilter]?.received ??
|
|
224
|
+
({ [stages[0]?.label ?? "Received"]: totalReceived } as Record<
|
|
225
|
+
string,
|
|
226
|
+
number
|
|
227
|
+
>)
|
|
228
|
+
|
|
229
|
+
const segments = Object.entries(breakdown).map(([name, value], index) => ({
|
|
230
|
+
id: name,
|
|
231
|
+
nodeColor: SEGMENT_PALETTE[index % SEGMENT_PALETTE.length],
|
|
232
|
+
value,
|
|
233
|
+
}))
|
|
234
|
+
|
|
235
|
+
const dropOffKeys = Object.keys(dropOffDistribution)
|
|
236
|
+
const dropOffNodes = dropOffKeys.length > 0
|
|
237
|
+
? dropOffKeys.map((reason) => ({
|
|
238
|
+
id: reason,
|
|
239
|
+
nodeColor: dropOffNodeColor ?? "#F59E0B",
|
|
240
|
+
}))
|
|
241
|
+
: DROP_OFF_NODES
|
|
242
|
+
|
|
243
|
+
const nodes = [
|
|
244
|
+
...segments,
|
|
245
|
+
...flowNodes,
|
|
246
|
+
...dropOffNodes,
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
const nodeIds = new Set(nodes.map((n) => n.id))
|
|
250
|
+
for (const link of flowLinks) {
|
|
251
|
+
for (const endpoint of [link.source, link.target]) {
|
|
252
|
+
if (!nodeIds.has(endpoint)) {
|
|
253
|
+
nodes.push({ id: endpoint, nodeColor: dropOffNodeColor ?? "#F59E0B" })
|
|
254
|
+
nodeIds.add(endpoint)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const links: { source: string; target: string; value: number }[] = []
|
|
260
|
+
|
|
261
|
+
const firstFlowNode = flowNodes[0]?.id ?? "Contacted"
|
|
262
|
+
|
|
263
|
+
segments.forEach((segment) => {
|
|
264
|
+
if (segment.value > 0) {
|
|
265
|
+
links.push({
|
|
266
|
+
source: segment.id,
|
|
267
|
+
target: firstFlowNode,
|
|
268
|
+
value: segment.value,
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
for (const [reason, count] of Object.entries(dropOffDistribution)) {
|
|
274
|
+
if (count > 0) {
|
|
275
|
+
links.push({ source: firstFlowNode, target: reason, value: count })
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
flowLinks.forEach((link) => links.push({ ...link }))
|
|
280
|
+
|
|
281
|
+
return { nodes, links }
|
|
282
|
+
}, [
|
|
283
|
+
selectedFilter,
|
|
284
|
+
filterBreakdowns,
|
|
285
|
+
stages,
|
|
286
|
+
totalReceived,
|
|
287
|
+
flowNodes,
|
|
288
|
+
dropOffDistribution,
|
|
289
|
+
dropOffNodeColor,
|
|
290
|
+
flowLinks,
|
|
291
|
+
])
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<div
|
|
295
|
+
className={cn(
|
|
296
|
+
"rounded-xl border border-border bg-card p-5 shadow-sm",
|
|
297
|
+
className,
|
|
298
|
+
)}
|
|
299
|
+
>
|
|
300
|
+
{/* Header */}
|
|
301
|
+
<div className="mb-6 flex flex-col gap-4">
|
|
302
|
+
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
303
|
+
<div className="flex items-center gap-4">
|
|
304
|
+
<h3 className="text-lg font-semibold text-foreground">{title}</h3>
|
|
305
|
+
|
|
306
|
+
{/* Counting Mode Toggle */}
|
|
307
|
+
{countingModes.length > 1 && (
|
|
308
|
+
<div className="flex items-center gap-2 rounded-lg border border-border bg-muted p-1">
|
|
309
|
+
{countingModes.map((mode) => (
|
|
310
|
+
<button
|
|
311
|
+
key={mode}
|
|
312
|
+
onClick={() => setCountingMode(mode)}
|
|
313
|
+
className={cn(
|
|
314
|
+
"rounded-md px-3 py-1 text-xs font-medium transition-all",
|
|
315
|
+
countingMode === mode
|
|
316
|
+
? "border border-border bg-background text-foreground shadow-sm"
|
|
317
|
+
: "text-muted-foreground hover:text-foreground",
|
|
318
|
+
)}
|
|
319
|
+
>
|
|
320
|
+
{mode}
|
|
321
|
+
</button>
|
|
322
|
+
))}
|
|
323
|
+
<TooltipProvider>
|
|
324
|
+
<Tooltip>
|
|
325
|
+
<TooltipTrigger asChild>
|
|
326
|
+
<Info className="ml-1 h-3.5 w-3.5 cursor-help text-muted-foreground/70" />
|
|
327
|
+
</TooltipTrigger>
|
|
328
|
+
<TooltipContent className="max-w-[250px] text-xs">
|
|
329
|
+
{countingModeTooltip}
|
|
330
|
+
</TooltipContent>
|
|
331
|
+
</Tooltip>
|
|
332
|
+
</TooltipProvider>
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
{/* Filter Tabs */}
|
|
338
|
+
{filterOptions.length > 1 && (
|
|
339
|
+
<div className="flex items-center gap-1 self-start rounded-lg bg-muted p-1 md:self-auto">
|
|
340
|
+
{filterOptions.map((option) => (
|
|
341
|
+
<button
|
|
342
|
+
key={option}
|
|
343
|
+
onClick={() => { setSelectedFilter(option); onFilterChange?.(option); }}
|
|
344
|
+
className={cn(
|
|
345
|
+
"h-7 rounded-md border-none bg-transparent px-3 text-xs font-medium shadow-none transition-all hover:bg-background",
|
|
346
|
+
selectedFilter === option &&
|
|
347
|
+
"bg-background text-foreground shadow-sm",
|
|
348
|
+
)}
|
|
349
|
+
>
|
|
350
|
+
{option}
|
|
351
|
+
</button>
|
|
352
|
+
))}
|
|
353
|
+
</div>
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
{/* Stage Metrics Row */}
|
|
359
|
+
<div className="mb-2 grid gap-4" style={{ gridTemplateColumns: `repeat(${stages.length}, minmax(0, 1fr))` }}>
|
|
360
|
+
{stages.map((stage, index) => {
|
|
361
|
+
const details = stageMetrics[stage.id]
|
|
362
|
+
const timing = stageTimings[index] ?? null
|
|
363
|
+
|
|
364
|
+
return (
|
|
365
|
+
<TooltipProvider key={stage.id} delayDuration={100}>
|
|
366
|
+
<Tooltip>
|
|
367
|
+
<TooltipTrigger asChild>
|
|
368
|
+
<div className="group relative flex cursor-pointer flex-col items-center rounded-lg p-2 text-center transition-colors hover:bg-muted">
|
|
369
|
+
<div
|
|
370
|
+
className="mb-1 w-full truncate text-xs font-medium text-muted-foreground"
|
|
371
|
+
title={stage.label}
|
|
372
|
+
>
|
|
373
|
+
{stage.label}
|
|
374
|
+
</div>
|
|
375
|
+
<div className="mb-1 text-2xl font-bold text-foreground">
|
|
376
|
+
{stage.count.toLocaleString()}
|
|
377
|
+
</div>
|
|
378
|
+
<div className="mb-1 flex items-center justify-center gap-1 text-xs font-medium text-emerald-600">
|
|
379
|
+
{stage.trend} <TrendingUp className="h-3 w-3" />
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
{/* Conversion badge + timing between stages */}
|
|
383
|
+
{index < stages.length - 1 && stage.nextConversion && (
|
|
384
|
+
<div className={cn(
|
|
385
|
+
alwaysShowConversionBadges ? "flex" : "hidden xl:flex",
|
|
386
|
+
"absolute -right-2 top-1/2 z-10 -translate-y-1/2 translate-x-1/2 flex-col items-center",
|
|
387
|
+
)}>
|
|
388
|
+
<span className="z-10 whitespace-nowrap rounded-full border border-border bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground shadow-sm">
|
|
389
|
+
{stage.nextConversion}
|
|
390
|
+
</span>
|
|
391
|
+
{timing && (
|
|
392
|
+
<div className="mt-1 flex flex-col items-center">
|
|
393
|
+
<div className="h-2 w-px bg-border" />
|
|
394
|
+
<div className="whitespace-nowrap rounded bg-background/80 px-1 py-0.5 text-[9px] font-medium leading-3 text-muted-foreground/70 backdrop-blur-[2px]">
|
|
395
|
+
<span className="mr-1 font-semibold text-muted-foreground">
|
|
396
|
+
Med: {timing.median}
|
|
397
|
+
</span>
|
|
398
|
+
<span className="text-muted-foreground/70">
|
|
399
|
+
Avg: {timing.avg}
|
|
400
|
+
</span>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
|
|
407
|
+
{/* Connector line down to Sankey */}
|
|
408
|
+
<div className="mx-auto mb-1 mt-2 h-4 w-px bg-border" />
|
|
409
|
+
</div>
|
|
410
|
+
</TooltipTrigger>
|
|
411
|
+
<TooltipContent
|
|
412
|
+
className="border-none bg-transparent p-0 shadow-none"
|
|
413
|
+
sideOffset={8}
|
|
414
|
+
>
|
|
415
|
+
<StageHoverCard
|
|
416
|
+
title={stage.label}
|
|
417
|
+
count={stage.count}
|
|
418
|
+
metrics={details}
|
|
419
|
+
/>
|
|
420
|
+
</TooltipContent>
|
|
421
|
+
</Tooltip>
|
|
422
|
+
</TooltipProvider>
|
|
423
|
+
)
|
|
424
|
+
})}
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
{/* Sankey Chart */}
|
|
428
|
+
<div
|
|
429
|
+
className="relative mt-4 w-full"
|
|
430
|
+
style={{ height: 400, minWidth: 0 }}
|
|
431
|
+
>
|
|
432
|
+
<ResponsiveSankey
|
|
433
|
+
data={sankeyData}
|
|
434
|
+
margin={effectiveMargin}
|
|
435
|
+
align="justify"
|
|
436
|
+
colors={(node: { nodeColor?: string }) => node.nodeColor || "#94a3b8"}
|
|
437
|
+
nodeOpacity={1}
|
|
438
|
+
nodeHoverOthersOpacity={0.35}
|
|
439
|
+
nodeThickness={18}
|
|
440
|
+
nodeSpacing={16}
|
|
441
|
+
nodeBorderWidth={0}
|
|
442
|
+
nodeBorderRadius={3}
|
|
443
|
+
linkOpacity={0.5}
|
|
444
|
+
linkHoverOthersOpacity={0.1}
|
|
445
|
+
linkContract={3}
|
|
446
|
+
enableLinkGradient
|
|
447
|
+
labelPosition="outside"
|
|
448
|
+
labelOrientation="horizontal"
|
|
449
|
+
labelPadding={effectiveLabelPadding}
|
|
450
|
+
label={(node: { id: string }) => nodeAmounts?.[node.id] ? `${node.id} (${nodeAmounts[node.id]})` : node.id}
|
|
451
|
+
labelTextColor={{ from: "color", modifiers: [["darker", 1]] }}
|
|
452
|
+
nodeTooltip={({ node }: { node: { id: string; value: number } }) => {
|
|
453
|
+
const unit = effectiveTerminalNodeIds.has(node.id) ? effectiveTerminalUnitLabel : effectiveUnitLabel
|
|
454
|
+
return (
|
|
455
|
+
<div className="rounded-md border border-border bg-card p-2 text-xs shadow-lg">
|
|
456
|
+
<span className="mb-1 block font-bold">{node.id}{nodeAmounts?.[node.id] ? ` — ${nodeAmounts[node.id]}` : ""}</span>
|
|
457
|
+
<span>{node.value} {unit}</span>
|
|
458
|
+
</div>
|
|
459
|
+
)
|
|
460
|
+
}}
|
|
461
|
+
linkTooltip={({ link }: { link: { source: { id: string }; target: { id: string }; value: number } }) => {
|
|
462
|
+
const unit = effectiveTerminalNodeIds.has(link.target.id) ? effectiveTerminalUnitLabel : effectiveUnitLabel
|
|
463
|
+
return (
|
|
464
|
+
<div className="rounded-md border border-border bg-card p-2 text-xs shadow-lg">
|
|
465
|
+
<span className="mb-1 block font-bold">
|
|
466
|
+
{link.source.id} → {link.target.id}
|
|
467
|
+
</span>
|
|
468
|
+
<span>{link.value} {unit}</span>
|
|
469
|
+
</div>
|
|
470
|
+
)
|
|
471
|
+
}}
|
|
472
|
+
/>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
)
|
|
476
|
+
}
|