@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,843 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import {
|
|
5
|
+
AlertCircle,
|
|
6
|
+
ChevronDown,
|
|
7
|
+
Mail,
|
|
8
|
+
MessageSquare,
|
|
9
|
+
Phone,
|
|
10
|
+
SearchX,
|
|
11
|
+
} from "lucide-react"
|
|
12
|
+
|
|
13
|
+
import { cn } from "../lib/utils"
|
|
14
|
+
import { Badge } from "./badge"
|
|
15
|
+
import {
|
|
16
|
+
ItemListToolbar,
|
|
17
|
+
type ItemListQuickView,
|
|
18
|
+
} from "./item-list-toolbar"
|
|
19
|
+
import { type ItemListDisplayState } from "./item-list-display"
|
|
20
|
+
import { type ItemListFilterCategory } from "./item-list-filter"
|
|
21
|
+
|
|
22
|
+
type QueueStage =
|
|
23
|
+
| "Referrals"
|
|
24
|
+
| "E&B Verified"
|
|
25
|
+
| "Contacted"
|
|
26
|
+
| "Pending Intake"
|
|
27
|
+
| "Pending Scheduling"
|
|
28
|
+
|
|
29
|
+
type QueueRisk = "At Risk" | "Watch" | "Stable"
|
|
30
|
+
|
|
31
|
+
interface QueueItem {
|
|
32
|
+
id: string
|
|
33
|
+
patient: string
|
|
34
|
+
source: string
|
|
35
|
+
specialty: string
|
|
36
|
+
stage: QueueStage
|
|
37
|
+
risk: QueueRisk
|
|
38
|
+
attempts: number
|
|
39
|
+
owner: string
|
|
40
|
+
aging: string
|
|
41
|
+
contactSignals: {
|
|
42
|
+
phone: boolean
|
|
43
|
+
email: boolean
|
|
44
|
+
message: boolean
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const QUEUE_ITEMS: QueueItem[] = [
|
|
49
|
+
{
|
|
50
|
+
id: "REF-1894",
|
|
51
|
+
patient: "James Liu",
|
|
52
|
+
source: "Cedars",
|
|
53
|
+
specialty: "Oncology",
|
|
54
|
+
stage: "E&B Verified",
|
|
55
|
+
risk: "At Risk",
|
|
56
|
+
attempts: 2,
|
|
57
|
+
owner: "Jessica Wong",
|
|
58
|
+
aging: "Aging 18h",
|
|
59
|
+
contactSignals: { phone: true, email: false, message: false },
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "REF-1903",
|
|
63
|
+
patient: "Michael Brown",
|
|
64
|
+
source: "Providence",
|
|
65
|
+
specialty: "Orthopedics",
|
|
66
|
+
stage: "E&B Verified",
|
|
67
|
+
risk: "Stable",
|
|
68
|
+
attempts: 1,
|
|
69
|
+
owner: "Sarah Johnson",
|
|
70
|
+
aging: "New today",
|
|
71
|
+
contactSignals: { phone: false, email: true, message: false },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "REF-1910",
|
|
75
|
+
patient: "Samantha Rodriguez",
|
|
76
|
+
source: "Kaiser",
|
|
77
|
+
specialty: "Rheumatology",
|
|
78
|
+
stage: "E&B Verified",
|
|
79
|
+
risk: "At Risk",
|
|
80
|
+
attempts: 4,
|
|
81
|
+
owner: "Michael Chen",
|
|
82
|
+
aging: "Aging 36h",
|
|
83
|
+
contactSignals: { phone: true, email: true, message: true },
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "REF-1916",
|
|
87
|
+
patient: "Christopher Davis",
|
|
88
|
+
source: "USC",
|
|
89
|
+
specialty: "Gastroenterology",
|
|
90
|
+
stage: "E&B Verified",
|
|
91
|
+
risk: "Stable",
|
|
92
|
+
attempts: 1,
|
|
93
|
+
owner: "Daniel Kim",
|
|
94
|
+
aging: "New 6h ago",
|
|
95
|
+
contactSignals: { phone: false, email: true, message: true },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "REF-1924",
|
|
99
|
+
patient: "Lisa Anderson",
|
|
100
|
+
source: "CHLA",
|
|
101
|
+
specialty: "Pediatric Surgery",
|
|
102
|
+
stage: "E&B Verified",
|
|
103
|
+
risk: "Watch",
|
|
104
|
+
attempts: 1,
|
|
105
|
+
owner: "Jennifer Davis",
|
|
106
|
+
aging: "New 8h ago",
|
|
107
|
+
contactSignals: { phone: true, email: false, message: true },
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "REF-1901",
|
|
111
|
+
patient: "Sarah Chen",
|
|
112
|
+
source: "CHLA",
|
|
113
|
+
specialty: "Pediatrics",
|
|
114
|
+
stage: "Contacted",
|
|
115
|
+
risk: "Stable",
|
|
116
|
+
attempts: 1,
|
|
117
|
+
owner: "Daniel Kim",
|
|
118
|
+
aging: "New today",
|
|
119
|
+
contactSignals: { phone: true, email: false, message: false },
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "REF-1908",
|
|
123
|
+
patient: "David Smith",
|
|
124
|
+
source: "USC",
|
|
125
|
+
specialty: "Dermatology",
|
|
126
|
+
stage: "Contacted",
|
|
127
|
+
risk: "Stable",
|
|
128
|
+
attempts: 2,
|
|
129
|
+
owner: "Jennifer Davis",
|
|
130
|
+
aging: "New 3h ago",
|
|
131
|
+
contactSignals: { phone: false, email: true, message: true },
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "REF-1913",
|
|
135
|
+
patient: "Olivia Martinez",
|
|
136
|
+
source: "Providence",
|
|
137
|
+
specialty: "Cardiology",
|
|
138
|
+
stage: "Contacted",
|
|
139
|
+
risk: "At Risk",
|
|
140
|
+
attempts: 3,
|
|
141
|
+
owner: "Jessica Wong",
|
|
142
|
+
aging: "Aging 24h",
|
|
143
|
+
contactSignals: { phone: true, email: true, message: false },
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "REF-1919",
|
|
147
|
+
patient: "Kevin Nguyen",
|
|
148
|
+
source: "Kaiser",
|
|
149
|
+
specialty: "Nephrology",
|
|
150
|
+
stage: "Contacted",
|
|
151
|
+
risk: "Stable",
|
|
152
|
+
attempts: 1,
|
|
153
|
+
owner: "Sarah Johnson",
|
|
154
|
+
aging: "New 5h ago",
|
|
155
|
+
contactSignals: { phone: true, email: false, message: true },
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "REF-1923",
|
|
159
|
+
patient: "Rachel Kim",
|
|
160
|
+
source: "UCLA Health",
|
|
161
|
+
specialty: "Hematology",
|
|
162
|
+
stage: "Contacted",
|
|
163
|
+
risk: "Watch",
|
|
164
|
+
attempts: 2,
|
|
165
|
+
owner: "Michael Chen",
|
|
166
|
+
aging: "New 7h ago",
|
|
167
|
+
contactSignals: { phone: false, email: true, message: true },
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: "REF-1926",
|
|
171
|
+
patient: "Brian Foster",
|
|
172
|
+
source: "Cedars",
|
|
173
|
+
specialty: "Vascular Surgery",
|
|
174
|
+
stage: "Contacted",
|
|
175
|
+
risk: "Stable",
|
|
176
|
+
attempts: 1,
|
|
177
|
+
owner: "Daniel Kim",
|
|
178
|
+
aging: "New today",
|
|
179
|
+
contactSignals: { phone: false, email: false, message: true },
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: "REF-1931",
|
|
183
|
+
patient: "Jasmine Patel",
|
|
184
|
+
source: "Dignity Health",
|
|
185
|
+
specialty: "Pulmonology",
|
|
186
|
+
stage: "Pending Intake",
|
|
187
|
+
risk: "Watch",
|
|
188
|
+
attempts: 2,
|
|
189
|
+
owner: "Sarah Johnson",
|
|
190
|
+
aging: "Aging 14h",
|
|
191
|
+
contactSignals: { phone: true, email: true, message: false },
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: "REF-1933",
|
|
195
|
+
patient: "Evan Torres",
|
|
196
|
+
source: "Sutter",
|
|
197
|
+
specialty: "Neurology",
|
|
198
|
+
stage: "Pending Intake",
|
|
199
|
+
risk: "Stable",
|
|
200
|
+
attempts: 1,
|
|
201
|
+
owner: "Jessica Wong",
|
|
202
|
+
aging: "New 2h ago",
|
|
203
|
+
contactSignals: { phone: false, email: true, message: true },
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "REF-1940",
|
|
207
|
+
patient: "Natalie Brooks",
|
|
208
|
+
source: "MemorialCare",
|
|
209
|
+
specialty: "Pain Management",
|
|
210
|
+
stage: "Pending Scheduling",
|
|
211
|
+
risk: "Watch",
|
|
212
|
+
attempts: 2,
|
|
213
|
+
owner: "Jennifer Davis",
|
|
214
|
+
aging: "Aging 20h",
|
|
215
|
+
contactSignals: { phone: true, email: false, message: true },
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: "REF-1942",
|
|
219
|
+
patient: "Carlos Rivera",
|
|
220
|
+
source: "Providence",
|
|
221
|
+
specialty: "Urology",
|
|
222
|
+
stage: "Pending Scheduling",
|
|
223
|
+
risk: "Stable",
|
|
224
|
+
attempts: 1,
|
|
225
|
+
owner: "Daniel Kim",
|
|
226
|
+
aging: "New 5h ago",
|
|
227
|
+
contactSignals: { phone: false, email: true, message: false },
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: "REF-1946",
|
|
231
|
+
patient: "Alyssa Wright",
|
|
232
|
+
source: "UCLA Health",
|
|
233
|
+
specialty: "Referrals",
|
|
234
|
+
stage: "Referrals",
|
|
235
|
+
risk: "Stable",
|
|
236
|
+
attempts: 1,
|
|
237
|
+
owner: "Michael Chen",
|
|
238
|
+
aging: "New today",
|
|
239
|
+
contactSignals: { phone: false, email: true, message: true },
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: "REF-1948",
|
|
243
|
+
patient: "Marcus Lopez",
|
|
244
|
+
source: "Cedars",
|
|
245
|
+
specialty: "Referrals",
|
|
246
|
+
stage: "Referrals",
|
|
247
|
+
risk: "At Risk",
|
|
248
|
+
attempts: 4,
|
|
249
|
+
owner: "Jessica Wong",
|
|
250
|
+
aging: "Aging 30h",
|
|
251
|
+
contactSignals: { phone: true, email: true, message: false },
|
|
252
|
+
},
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
const STAGE_ORDER: QueueStage[] = [
|
|
256
|
+
"Referrals",
|
|
257
|
+
"E&B Verified",
|
|
258
|
+
"Contacted",
|
|
259
|
+
"Pending Intake",
|
|
260
|
+
"Pending Scheduling",
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
const RISK_ORDER: QueueRisk[] = ["At Risk", "Watch", "Stable"]
|
|
264
|
+
|
|
265
|
+
const FILTER_CATEGORIES: ItemListFilterCategory[] = [
|
|
266
|
+
{
|
|
267
|
+
id: "stage",
|
|
268
|
+
label: "Queue stage",
|
|
269
|
+
options: STAGE_ORDER,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: "risk",
|
|
273
|
+
label: "Risk level",
|
|
274
|
+
options: RISK_ORDER,
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
id: "owner",
|
|
278
|
+
label: "Assignee",
|
|
279
|
+
options: [...new Set(QUEUE_ITEMS.map((item) => item.owner))],
|
|
280
|
+
},
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
interface QuickViewDefinition {
|
|
284
|
+
id: string
|
|
285
|
+
label: string
|
|
286
|
+
matches: (item: QueueItem) => boolean
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const QUICK_VIEW_DEFINITIONS: QuickViewDefinition[] = [
|
|
290
|
+
{
|
|
291
|
+
id: "referrals",
|
|
292
|
+
label: "Referrals",
|
|
293
|
+
matches: (item) => item.stage === "Referrals",
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
id: "contact-attempted",
|
|
297
|
+
label: "Contact Attempted",
|
|
298
|
+
matches: (item) =>
|
|
299
|
+
item.stage === "E&B Verified" || item.stage === "Contacted",
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
id: "pending-intake",
|
|
303
|
+
label: "Pending Intake",
|
|
304
|
+
matches: (item) => item.stage === "Pending Intake",
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
id: "pending-scheduling",
|
|
308
|
+
label: "Pending Scheduling",
|
|
309
|
+
matches: (item) => item.stage === "Pending Scheduling",
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: "needs-escalation",
|
|
313
|
+
label: "Needs Escalation",
|
|
314
|
+
matches: (item) => item.attempts >= 4 || item.risk === "At Risk",
|
|
315
|
+
},
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
const DEFAULT_DISPLAY_STATE: ItemListDisplayState = {
|
|
319
|
+
viewMode: "list",
|
|
320
|
+
grouping: "stage",
|
|
321
|
+
subGrouping: "none",
|
|
322
|
+
ordering: "priority",
|
|
323
|
+
orderingDirection: "desc",
|
|
324
|
+
showContactSignals: true,
|
|
325
|
+
showOwner: true,
|
|
326
|
+
showStatus: true,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function toggleFilterValue(
|
|
330
|
+
current: Record<string, string[]>,
|
|
331
|
+
categoryId: string,
|
|
332
|
+
option: string,
|
|
333
|
+
) {
|
|
334
|
+
const selected = current[categoryId] ?? []
|
|
335
|
+
const hasOption = selected.includes(option)
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
...current,
|
|
339
|
+
[categoryId]: hasOption
|
|
340
|
+
? selected.filter((value) => value !== option)
|
|
341
|
+
: [...selected, option],
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function matchesCategoryFilter(
|
|
346
|
+
item: QueueItem,
|
|
347
|
+
categoryId: string,
|
|
348
|
+
selectedOptions: string[],
|
|
349
|
+
) {
|
|
350
|
+
if (selectedOptions.length === 0) {
|
|
351
|
+
return true
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
switch (categoryId) {
|
|
355
|
+
case "stage":
|
|
356
|
+
return selectedOptions.includes(item.stage)
|
|
357
|
+
case "risk":
|
|
358
|
+
return selectedOptions.includes(item.risk)
|
|
359
|
+
case "owner":
|
|
360
|
+
return selectedOptions.includes(item.owner)
|
|
361
|
+
default:
|
|
362
|
+
return true
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function getRiskDotColor(risk: QueueRisk) {
|
|
367
|
+
switch (risk) {
|
|
368
|
+
case "At Risk":
|
|
369
|
+
return "bg-red-500"
|
|
370
|
+
case "Watch":
|
|
371
|
+
return "bg-orange-400"
|
|
372
|
+
case "Stable":
|
|
373
|
+
default:
|
|
374
|
+
return "bg-muted-foreground/30"
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const AVATAR_COLORS = [
|
|
379
|
+
"bg-blue-100 text-blue-700",
|
|
380
|
+
"bg-emerald-100 text-emerald-700",
|
|
381
|
+
"bg-purple-100 text-purple-700",
|
|
382
|
+
"bg-amber-100 text-amber-700",
|
|
383
|
+
"bg-rose-100 text-rose-700",
|
|
384
|
+
"bg-cyan-100 text-cyan-700",
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
function getAvatarColor(name: string) {
|
|
388
|
+
return AVATAR_COLORS[name.charCodeAt(0) % AVATAR_COLORS.length]
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function WorkItemCard({ item, display }: { item: QueueItem; display: ItemListDisplayState }) {
|
|
392
|
+
return (
|
|
393
|
+
<div className="rounded-lg border border-border bg-card p-3 shadow-sm transition-shadow hover:shadow-md">
|
|
394
|
+
<div className="mb-2 flex items-center justify-between">
|
|
395
|
+
<span className="font-mono text-[10px] text-muted-foreground">{item.id}</span>
|
|
396
|
+
<div
|
|
397
|
+
className={cn(
|
|
398
|
+
"h-2 w-2 rounded-full",
|
|
399
|
+
getRiskDotColor(item.risk),
|
|
400
|
+
)}
|
|
401
|
+
/>
|
|
402
|
+
</div>
|
|
403
|
+
<div className="mb-1 text-sm font-medium text-foreground">
|
|
404
|
+
{item.patient}
|
|
405
|
+
</div>
|
|
406
|
+
<div className="mb-2 text-xs text-muted-foreground">
|
|
407
|
+
via {item.source} → {item.specialty}
|
|
408
|
+
</div>
|
|
409
|
+
<div className="flex items-center justify-between">
|
|
410
|
+
<div className="flex items-center gap-1.5">
|
|
411
|
+
{item.risk === "At Risk" ? (
|
|
412
|
+
<Badge
|
|
413
|
+
variant="outline"
|
|
414
|
+
className="h-5 gap-1 rounded-full border-red-200 bg-red-50 px-1.5 text-[10px] font-medium text-red-700 shadow-none"
|
|
415
|
+
>
|
|
416
|
+
<AlertCircle className="h-3 w-3" />
|
|
417
|
+
At Risk
|
|
418
|
+
</Badge>
|
|
419
|
+
) : null}
|
|
420
|
+
<span
|
|
421
|
+
className={cn(
|
|
422
|
+
"inline-flex h-5 items-center rounded border px-1.5 text-[10px] font-semibold",
|
|
423
|
+
item.attempts >= 4
|
|
424
|
+
? "border-red-200 bg-red-50 text-red-700"
|
|
425
|
+
: item.attempts === 3
|
|
426
|
+
? "border-amber-200 bg-amber-50 text-amber-700"
|
|
427
|
+
: "border-border bg-muted text-muted-foreground",
|
|
428
|
+
)}
|
|
429
|
+
>
|
|
430
|
+
{item.attempts >= 4 ? "4+" : item.attempts}
|
|
431
|
+
</span>
|
|
432
|
+
</div>
|
|
433
|
+
<span className="text-[11px] text-muted-foreground">{item.aging}</span>
|
|
434
|
+
</div>
|
|
435
|
+
{display.showOwner ? (
|
|
436
|
+
<div className="mt-2 flex items-center gap-1.5 border-t border-border/60 pt-2">
|
|
437
|
+
<div
|
|
438
|
+
className={cn(
|
|
439
|
+
"flex h-5 w-5 items-center justify-center rounded text-[9px] font-semibold",
|
|
440
|
+
getAvatarColor(item.owner),
|
|
441
|
+
)}
|
|
442
|
+
>
|
|
443
|
+
{item.owner.charAt(0)}
|
|
444
|
+
</div>
|
|
445
|
+
<span className="text-xs text-muted-foreground">{item.owner}</span>
|
|
446
|
+
</div>
|
|
447
|
+
) : null}
|
|
448
|
+
</div>
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function ItemList() {
|
|
453
|
+
const [selectedFilters, setSelectedFilters] = React.useState<
|
|
454
|
+
Record<string, string[]>
|
|
455
|
+
>({})
|
|
456
|
+
const [activeQuickView, setActiveQuickView] = React.useState<string | null>(
|
|
457
|
+
"contact-attempted",
|
|
458
|
+
)
|
|
459
|
+
const [display, setDisplay] =
|
|
460
|
+
React.useState<ItemListDisplayState>(DEFAULT_DISPLAY_STATE)
|
|
461
|
+
const [expandedGroups, setExpandedGroups] = React.useState<
|
|
462
|
+
Record<string, boolean>
|
|
463
|
+
>({})
|
|
464
|
+
|
|
465
|
+
const quickViews = React.useMemo<ItemListQuickView[]>(
|
|
466
|
+
() =>
|
|
467
|
+
QUICK_VIEW_DEFINITIONS.map((definition) => ({
|
|
468
|
+
id: definition.id,
|
|
469
|
+
label: definition.label,
|
|
470
|
+
count: QUEUE_ITEMS.filter(definition.matches).length,
|
|
471
|
+
})),
|
|
472
|
+
[],
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
const filteredItems = React.useMemo(() => {
|
|
476
|
+
const quickViewFilter = QUICK_VIEW_DEFINITIONS.find(
|
|
477
|
+
(definition) => definition.id === activeQuickView,
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
return QUEUE_ITEMS.filter((item) => {
|
|
481
|
+
if (quickViewFilter && !quickViewFilter.matches(item)) {
|
|
482
|
+
return false
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return Object.entries(selectedFilters).every(([categoryId, options]) =>
|
|
486
|
+
matchesCategoryFilter(item, categoryId, options),
|
|
487
|
+
)
|
|
488
|
+
})
|
|
489
|
+
}, [activeQuickView, selectedFilters])
|
|
490
|
+
|
|
491
|
+
const groupedItems = React.useMemo(() => {
|
|
492
|
+
const groups = new Map<string, QueueItem[]>()
|
|
493
|
+
|
|
494
|
+
for (const item of filteredItems) {
|
|
495
|
+
const key =
|
|
496
|
+
display.grouping === "stage"
|
|
497
|
+
? item.stage
|
|
498
|
+
: display.grouping === "owner"
|
|
499
|
+
? item.owner
|
|
500
|
+
: item.risk
|
|
501
|
+
const current = groups.get(key) ?? []
|
|
502
|
+
current.push(item)
|
|
503
|
+
groups.set(key, current)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const entries = Array.from(groups.entries())
|
|
507
|
+
|
|
508
|
+
entries.sort(([groupA], [groupB]) => {
|
|
509
|
+
if (display.grouping === "stage") {
|
|
510
|
+
return (
|
|
511
|
+
STAGE_ORDER.indexOf(groupA as QueueStage) -
|
|
512
|
+
STAGE_ORDER.indexOf(groupB as QueueStage)
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (display.grouping === "risk") {
|
|
517
|
+
return (
|
|
518
|
+
RISK_ORDER.indexOf(groupA as QueueRisk) -
|
|
519
|
+
RISK_ORDER.indexOf(groupB as QueueRisk)
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return groupA.localeCompare(groupB)
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
return entries.map(([group, items]) => ({
|
|
527
|
+
key: group.toLowerCase().replace(/\s+/g, "-"),
|
|
528
|
+
label: group,
|
|
529
|
+
items,
|
|
530
|
+
}))
|
|
531
|
+
}, [display.grouping, filteredItems])
|
|
532
|
+
|
|
533
|
+
React.useEffect(() => {
|
|
534
|
+
setExpandedGroups((previous) => {
|
|
535
|
+
const next: Record<string, boolean> = {}
|
|
536
|
+
groupedItems.forEach((group) => {
|
|
537
|
+
next[group.key] = previous[group.key] ?? true
|
|
538
|
+
})
|
|
539
|
+
return next
|
|
540
|
+
})
|
|
541
|
+
}, [groupedItems])
|
|
542
|
+
|
|
543
|
+
return (
|
|
544
|
+
<div className="overflow-hidden bg-background">
|
|
545
|
+
<div className="border-b border-border px-6 py-4">
|
|
546
|
+
<h3 className="text-xl font-semibold tracking-tight text-foreground">
|
|
547
|
+
Patient Inventory
|
|
548
|
+
</h3>
|
|
549
|
+
</div>
|
|
550
|
+
|
|
551
|
+
<ItemListToolbar
|
|
552
|
+
quickViews={quickViews}
|
|
553
|
+
activeQuickView={activeQuickView}
|
|
554
|
+
onQuickViewChange={setActiveQuickView}
|
|
555
|
+
filterCategories={FILTER_CATEGORIES}
|
|
556
|
+
selectedFilters={selectedFilters}
|
|
557
|
+
onToggleFilter={(categoryId, option) =>
|
|
558
|
+
setSelectedFilters((previous) =>
|
|
559
|
+
toggleFilterValue(previous, categoryId, option),
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
onClearFilters={() => setSelectedFilters({})}
|
|
563
|
+
display={display}
|
|
564
|
+
onDisplayChange={setDisplay}
|
|
565
|
+
onResetDisplay={() => setDisplay(DEFAULT_DISPLAY_STATE)}
|
|
566
|
+
/>
|
|
567
|
+
|
|
568
|
+
<div className="flex-1 overflow-auto">
|
|
569
|
+
{groupedItems.length > 0 ? (
|
|
570
|
+
display.viewMode === "board" ? (
|
|
571
|
+
/* Board View */
|
|
572
|
+
<div className="flex gap-4 overflow-x-auto p-4">
|
|
573
|
+
{groupedItems.map((group) => (
|
|
574
|
+
<div
|
|
575
|
+
key={group.key}
|
|
576
|
+
className="flex w-[280px] shrink-0 flex-col"
|
|
577
|
+
>
|
|
578
|
+
<div className="mb-3 flex items-center justify-between px-1">
|
|
579
|
+
<h4 className="truncate text-sm font-semibold uppercase text-muted-foreground">
|
|
580
|
+
{group.label}
|
|
581
|
+
</h4>
|
|
582
|
+
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
|
|
583
|
+
{group.items.length}
|
|
584
|
+
</span>
|
|
585
|
+
</div>
|
|
586
|
+
<div className="flex flex-col gap-3">
|
|
587
|
+
{group.items.map((item) => (
|
|
588
|
+
<WorkItemCard
|
|
589
|
+
key={item.id}
|
|
590
|
+
item={item}
|
|
591
|
+
display={display}
|
|
592
|
+
/>
|
|
593
|
+
))}
|
|
594
|
+
{group.items.length === 0 ? (
|
|
595
|
+
<div className="flex h-24 items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted/30">
|
|
596
|
+
<span className="text-xs text-muted-foreground/50">
|
|
597
|
+
No items
|
|
598
|
+
</span>
|
|
599
|
+
</div>
|
|
600
|
+
) : null}
|
|
601
|
+
</div>
|
|
602
|
+
</div>
|
|
603
|
+
))}
|
|
604
|
+
</div>
|
|
605
|
+
) : (
|
|
606
|
+
/* List View */
|
|
607
|
+
groupedItems.map((group) => {
|
|
608
|
+
const isExpanded = expandedGroups[group.key]
|
|
609
|
+
|
|
610
|
+
return (
|
|
611
|
+
<div
|
|
612
|
+
key={group.key}
|
|
613
|
+
className="last:border-b-0"
|
|
614
|
+
>
|
|
615
|
+
<button
|
|
616
|
+
type="button"
|
|
617
|
+
onClick={() =>
|
|
618
|
+
setExpandedGroups((previous) => ({
|
|
619
|
+
...previous,
|
|
620
|
+
[group.key]: !previous[group.key],
|
|
621
|
+
}))
|
|
622
|
+
}
|
|
623
|
+
className="flex w-full cursor-pointer select-none items-center gap-2 border-b border-border bg-muted/30 px-4 py-2 text-left transition-colors hover:bg-muted/50"
|
|
624
|
+
>
|
|
625
|
+
<ChevronDown
|
|
626
|
+
className={cn(
|
|
627
|
+
"h-3.5 w-3.5 text-muted-foreground transition-transform",
|
|
628
|
+
!isExpanded && "-rotate-90",
|
|
629
|
+
)}
|
|
630
|
+
/>
|
|
631
|
+
<span className="text-[11px] font-bold uppercase tracking-wider text-muted-foreground">
|
|
632
|
+
{group.label}
|
|
633
|
+
</span>
|
|
634
|
+
<span className="rounded border border-border bg-background px-1.5 py-0 text-[10px] font-medium text-muted-foreground">
|
|
635
|
+
{group.items.length}
|
|
636
|
+
</span>
|
|
637
|
+
</button>
|
|
638
|
+
|
|
639
|
+
{isExpanded ? (
|
|
640
|
+
<div>
|
|
641
|
+
{group.items.map((item) => (
|
|
642
|
+
<div
|
|
643
|
+
key={item.id}
|
|
644
|
+
className="group flex cursor-pointer items-center gap-3 border-b border-border/50 px-4 py-2.5 text-sm transition-colors hover:bg-muted/40"
|
|
645
|
+
>
|
|
646
|
+
<span className="w-[80px] shrink-0 font-mono text-xs text-muted-foreground/80">
|
|
647
|
+
{item.id}
|
|
648
|
+
</span>
|
|
649
|
+
|
|
650
|
+
<div className="shrink-0">
|
|
651
|
+
<div
|
|
652
|
+
className={cn(
|
|
653
|
+
"h-2 w-2 rounded-full",
|
|
654
|
+
getRiskDotColor(item.risk),
|
|
655
|
+
)}
|
|
656
|
+
/>
|
|
657
|
+
</div>
|
|
658
|
+
|
|
659
|
+
<div className="min-w-0 flex-1 truncate font-semibold text-sm text-foreground">
|
|
660
|
+
{item.patient}{" "}
|
|
661
|
+
<span className="font-normal text-muted-foreground">
|
|
662
|
+
via {item.source} → {item.specialty}
|
|
663
|
+
</span>
|
|
664
|
+
</div>
|
|
665
|
+
|
|
666
|
+
<div className="hidden w-[80px] shrink-0 items-center md:flex">
|
|
667
|
+
{item.risk === "At Risk" ? (
|
|
668
|
+
<Badge
|
|
669
|
+
variant="outline"
|
|
670
|
+
className="h-5 gap-1 rounded-full border-red-200 bg-red-50 px-1.5 text-[10px] font-medium text-red-700 shadow-none"
|
|
671
|
+
>
|
|
672
|
+
<AlertCircle className="h-3 w-3" />
|
|
673
|
+
At Risk
|
|
674
|
+
</Badge>
|
|
675
|
+
) : null}
|
|
676
|
+
</div>
|
|
677
|
+
|
|
678
|
+
{display.showContactSignals ? (
|
|
679
|
+
<div className="hidden items-center gap-3 text-muted-foreground/40 md:flex">
|
|
680
|
+
<Phone className="h-3.5 w-3.5 cursor-pointer transition-colors hover:text-foreground" />
|
|
681
|
+
<Mail className="h-3.5 w-3.5 cursor-pointer transition-colors hover:text-foreground" />
|
|
682
|
+
<MessageSquare className="h-3.5 w-3.5 cursor-pointer transition-colors hover:text-foreground" />
|
|
683
|
+
</div>
|
|
684
|
+
) : null}
|
|
685
|
+
|
|
686
|
+
<div className="hidden w-[60px] shrink-0 items-center justify-center md:flex">
|
|
687
|
+
<span
|
|
688
|
+
className={cn(
|
|
689
|
+
"inline-flex h-5 items-center justify-center rounded border px-2 text-xs font-semibold",
|
|
690
|
+
item.attempts >= 4
|
|
691
|
+
? "border-red-300 bg-red-100 text-red-700"
|
|
692
|
+
: item.attempts === 3
|
|
693
|
+
? "border-amber-300 bg-amber-100 text-amber-700"
|
|
694
|
+
: "border-border bg-muted text-muted-foreground",
|
|
695
|
+
)}
|
|
696
|
+
>
|
|
697
|
+
{item.attempts >= 4 ? "4+" : item.attempts}
|
|
698
|
+
</span>
|
|
699
|
+
</div>
|
|
700
|
+
|
|
701
|
+
{display.showOwner ? (
|
|
702
|
+
<div className="hidden w-[120px] shrink-0 items-center gap-2 lg:flex">
|
|
703
|
+
<span className="truncate text-xs font-medium text-foreground">
|
|
704
|
+
{item.owner}
|
|
705
|
+
</span>
|
|
706
|
+
</div>
|
|
707
|
+
) : null}
|
|
708
|
+
|
|
709
|
+
{display.showStatus ? (
|
|
710
|
+
<div className="hidden w-[140px] shrink-0 items-center justify-end gap-2 sm:flex">
|
|
711
|
+
<span className="truncate rounded bg-muted px-2 py-0.5 text-xs text-muted-foreground">
|
|
712
|
+
{item.stage}
|
|
713
|
+
</span>
|
|
714
|
+
</div>
|
|
715
|
+
) : null}
|
|
716
|
+
|
|
717
|
+
<div className="w-[80px] shrink-0 text-right text-xs text-muted-foreground">
|
|
718
|
+
{item.aging}
|
|
719
|
+
</div>
|
|
720
|
+
</div>
|
|
721
|
+
))}
|
|
722
|
+
</div>
|
|
723
|
+
) : null}
|
|
724
|
+
</div>
|
|
725
|
+
)
|
|
726
|
+
})
|
|
727
|
+
)
|
|
728
|
+
) : (
|
|
729
|
+
<div className="flex h-56 flex-col items-center justify-center gap-1 text-muted-foreground">
|
|
730
|
+
<SearchX className="h-7 w-7 opacity-40" />
|
|
731
|
+
<p className="text-sm font-medium">No queue items found</p>
|
|
732
|
+
<p className="text-xs">
|
|
733
|
+
Try a different quick view or clear your filters.
|
|
734
|
+
</p>
|
|
735
|
+
</div>
|
|
736
|
+
)}
|
|
737
|
+
</div>
|
|
738
|
+
</div>
|
|
739
|
+
)
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// ---------------------------------------------------------------------------
|
|
743
|
+
// GroupedListView -- shared grouped row list used by ItemList and Inbox
|
|
744
|
+
// ---------------------------------------------------------------------------
|
|
745
|
+
|
|
746
|
+
export interface GroupedListGroup<T> {
|
|
747
|
+
key: string
|
|
748
|
+
label: string
|
|
749
|
+
items: T[]
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export interface GroupedListViewProps<T> {
|
|
753
|
+
groups: GroupedListGroup<T>[]
|
|
754
|
+
renderRow: (item: T, index: number) => React.ReactNode
|
|
755
|
+
getItemKey: (item: T) => string
|
|
756
|
+
selectedKey?: string
|
|
757
|
+
onItemClick?: (item: T) => void
|
|
758
|
+
emptyMessage?: string
|
|
759
|
+
className?: string
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export function GroupedListView<T>({
|
|
763
|
+
groups,
|
|
764
|
+
renderRow,
|
|
765
|
+
getItemKey,
|
|
766
|
+
selectedKey,
|
|
767
|
+
onItemClick,
|
|
768
|
+
emptyMessage = "No items found",
|
|
769
|
+
className,
|
|
770
|
+
}: GroupedListViewProps<T>) {
|
|
771
|
+
const [expandedGroups, setExpandedGroups] = React.useState<Record<string, boolean>>(() => {
|
|
772
|
+
const init: Record<string, boolean> = {}
|
|
773
|
+
groups.forEach((g) => { init[g.key] = true })
|
|
774
|
+
return init
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
React.useEffect(() => {
|
|
778
|
+
setExpandedGroups((prev) => {
|
|
779
|
+
const next: Record<string, boolean> = {}
|
|
780
|
+
groups.forEach((g) => { next[g.key] = prev[g.key] ?? true })
|
|
781
|
+
return next
|
|
782
|
+
})
|
|
783
|
+
}, [groups])
|
|
784
|
+
|
|
785
|
+
if (groups.length === 0 || groups.every((g) => g.items.length === 0)) {
|
|
786
|
+
return (
|
|
787
|
+
<div className={cn("flex h-56 flex-col items-center justify-center gap-1 text-muted-foreground", className)}>
|
|
788
|
+
<SearchX className="h-7 w-7 opacity-40" />
|
|
789
|
+
<p className="text-sm font-medium">{emptyMessage}</p>
|
|
790
|
+
</div>
|
|
791
|
+
)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return (
|
|
795
|
+
<div className={className}>
|
|
796
|
+
{groups.map((group) => {
|
|
797
|
+
const isExpanded = expandedGroups[group.key] ?? true
|
|
798
|
+
return (
|
|
799
|
+
<div key={group.key}>
|
|
800
|
+
<button
|
|
801
|
+
type="button"
|
|
802
|
+
onClick={() =>
|
|
803
|
+
setExpandedGroups((prev) => ({ ...prev, [group.key]: !prev[group.key] }))
|
|
804
|
+
}
|
|
805
|
+
className="flex w-full cursor-pointer select-none items-center gap-2 border-b border-border bg-muted/30 px-4 py-2 text-left transition-colors hover:bg-muted/50"
|
|
806
|
+
>
|
|
807
|
+
<ChevronDown
|
|
808
|
+
className={cn(
|
|
809
|
+
"h-3.5 w-3.5 text-muted-foreground transition-transform",
|
|
810
|
+
!isExpanded && "-rotate-90",
|
|
811
|
+
)}
|
|
812
|
+
/>
|
|
813
|
+
<span className="text-[11px] font-bold uppercase tracking-wider text-muted-foreground">
|
|
814
|
+
{group.label}
|
|
815
|
+
</span>
|
|
816
|
+
<span className="rounded border border-border bg-background px-1.5 py-0 text-[10px] font-medium text-muted-foreground">
|
|
817
|
+
{group.items.length}
|
|
818
|
+
</span>
|
|
819
|
+
</button>
|
|
820
|
+
{isExpanded &&
|
|
821
|
+
group.items.map((item, idx) => {
|
|
822
|
+
const key = getItemKey(item)
|
|
823
|
+
return (
|
|
824
|
+
<div
|
|
825
|
+
key={key}
|
|
826
|
+
onClick={() => onItemClick?.(item)}
|
|
827
|
+
className={cn(
|
|
828
|
+
"flex items-center gap-3 px-4 py-2.5 border-b border-border/50 text-[13px] cursor-pointer transition-colors",
|
|
829
|
+
selectedKey === key
|
|
830
|
+
? "bg-muted/30 border-l-2 border-l-primary"
|
|
831
|
+
: "border-l-2 border-l-transparent hover:bg-muted/40",
|
|
832
|
+
)}
|
|
833
|
+
>
|
|
834
|
+
{renderRow(item, idx)}
|
|
835
|
+
</div>
|
|
836
|
+
)
|
|
837
|
+
})}
|
|
838
|
+
</div>
|
|
839
|
+
)
|
|
840
|
+
})}
|
|
841
|
+
</div>
|
|
842
|
+
)
|
|
843
|
+
}
|