@wealthx/shadcn 1.2.2 → 1.3.1

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.
Files changed (230) hide show
  1. package/.turbo/turbo-build.log +193 -149
  2. package/CHANGELOG.md +28 -0
  3. package/dist/{chunk-4Y6R4WEC.mjs → chunk-2A5RRQGG.mjs} +9 -22
  4. package/dist/{chunk-TS2ZX2VS.mjs → chunk-2UM72RJ7.mjs} +11 -15
  5. package/dist/{chunk-A56YQQHG.mjs → chunk-3NCUZIFP.mjs} +2 -2
  6. package/dist/chunk-3OYFOX3X.mjs +79 -0
  7. package/dist/{chunk-RP3SQYA3.mjs → chunk-3TTACBDP.mjs} +9 -4
  8. package/dist/chunk-4GAWMKMI.mjs +710 -0
  9. package/dist/{chunk-VGSESELX.mjs → chunk-5FQIKDKP.mjs} +5 -5
  10. package/dist/{chunk-K3JYD4IU.mjs → chunk-5IS7G74I.mjs} +11 -4
  11. package/dist/chunk-6AW4KJHE.mjs +235 -0
  12. package/dist/chunk-6CR5N2JW.mjs +302 -0
  13. package/dist/{chunk-XIRTEFKH.mjs → chunk-6DZEXFNB.mjs} +36 -8
  14. package/dist/chunk-6O6KD7CE.mjs +271 -0
  15. package/dist/chunk-7PV3IWCN.mjs +33 -0
  16. package/dist/{chunk-SPJ5KXW7.mjs → chunk-7S5AESZO.mjs} +5 -5
  17. package/dist/{chunk-RYCLWMZ7.mjs → chunk-ABFDMHOR.mjs} +9 -7
  18. package/dist/{chunk-SWGT756Z.mjs → chunk-AMQZRHEZ.mjs} +10 -4
  19. package/dist/{chunk-WAZD7NFU.mjs → chunk-BKNFWEH2.mjs} +6 -6
  20. package/dist/{chunk-CLIN5525.mjs → chunk-C7CQJNMR.mjs} +1 -1
  21. package/dist/{chunk-D4ILTPOG.mjs → chunk-CFMQP5QS.mjs} +5 -4
  22. package/dist/{chunk-VPBN3WOO.mjs → chunk-DGHAXJBN.mjs} +9 -7
  23. package/dist/chunk-DOEO3CDL.mjs +27 -0
  24. package/dist/{chunk-5MEWU56Z.mjs → chunk-DUJTAXMH.mjs} +11 -6
  25. package/dist/{chunk-GGM2UYGG.mjs → chunk-EBXQWIYG.mjs} +10 -4
  26. package/dist/chunk-EWRB4PAD.mjs +468 -0
  27. package/dist/{chunk-ZSHYDDRB.mjs → chunk-FAKPBKLT.mjs} +6 -2
  28. package/dist/{chunk-A6AAWBPF.mjs → chunk-GHC7LLUX.mjs} +13 -4
  29. package/dist/chunk-HBZLGDIN.mjs +507 -0
  30. package/dist/{chunk-SIZMLSRU.mjs → chunk-HISNT2MG.mjs} +8 -6
  31. package/dist/{chunk-CGH4DRNG.mjs → chunk-HVY6KCCF.mjs} +10 -7
  32. package/dist/chunk-I3RZS7V2.mjs +136 -0
  33. package/dist/chunk-IAE3F7DR.mjs +1962 -0
  34. package/dist/{chunk-UT4KJR7V.mjs → chunk-IHMFS7NZ.mjs} +35 -74
  35. package/dist/{chunk-PCPLO5HT.mjs → chunk-IOJRDS6V.mjs} +96 -14
  36. package/dist/{chunk-LHYCMLVA.mjs → chunk-JKGDCQTZ.mjs} +11 -4
  37. package/dist/{chunk-H45TKD34.mjs → chunk-JMHR3YGZ.mjs} +1 -1
  38. package/dist/{chunk-4MN6UQHG.mjs → chunk-K5A5L6T2.mjs} +17 -39
  39. package/dist/chunk-LV35NGVG.mjs +272 -0
  40. package/dist/{chunk-FZIXGLMV.mjs → chunk-M3FV7LOK.mjs} +5 -12
  41. package/dist/{chunk-FMAXJ2SI.mjs → chunk-MBON7YRJ.mjs} +1 -1
  42. package/dist/chunk-MIZQHHUO.mjs +441 -0
  43. package/dist/chunk-MLNEWRWV.mjs +449 -0
  44. package/dist/chunk-MN5NYQCL.mjs +29 -0
  45. package/dist/chunk-NL3ZO62D.mjs +31 -0
  46. package/dist/{chunk-Q76O3RIQ.mjs → chunk-NMOI6CQD.mjs} +1 -1
  47. package/dist/{chunk-P6AM5V7O.mjs → chunk-OODBHKG7.mjs} +1 -1
  48. package/dist/chunk-PBL4OQV2.mjs +283 -0
  49. package/dist/{chunk-Y4QFWRNR.mjs → chunk-PU4YZQXV.mjs} +17 -18
  50. package/dist/chunk-Q2BGOAMG.mjs +202 -0
  51. package/dist/chunk-QMY3AZJH.mjs +80 -0
  52. package/dist/{chunk-BL3DXM2X.mjs → chunk-QZ4RE6NA.mjs} +11 -4
  53. package/dist/{chunk-VACKZOMY.mjs → chunk-R3VSPKNP.mjs} +3 -3
  54. package/dist/{chunk-OPNQAVVH.mjs → chunk-RJI6GKVF.mjs} +8 -6
  55. package/dist/{chunk-WG6JGJXB.mjs → chunk-T4BJLT57.mjs} +1 -1
  56. package/dist/chunk-UMTOX62O.mjs +415 -0
  57. package/dist/{chunk-7MMXNK3C.mjs → chunk-VLARHE5V.mjs} +8 -6
  58. package/dist/{chunk-2I5S2AMY.mjs → chunk-XREGSKX3.mjs} +2 -2
  59. package/dist/{chunk-JNQORUPP.mjs → chunk-YJG55G2H.mjs} +14 -11
  60. package/dist/components/ui/add-column-modal.js +42 -14
  61. package/dist/components/ui/add-column-modal.mjs +5 -5
  62. package/dist/components/ui/add-lead-modal.js +42 -11
  63. package/dist/components/ui/add-lead-modal.mjs +3 -3
  64. package/dist/components/ui/advisor-card.js +530 -0
  65. package/dist/components/ui/advisor-card.mjs +15 -0
  66. package/dist/components/ui/ai-assistant-drawer.js +11 -10
  67. package/dist/components/ui/ai-assistant-drawer.mjs +3 -3
  68. package/dist/components/ui/alert-dialog.js +2 -2
  69. package/dist/components/ui/alert-dialog.mjs +2 -2
  70. package/dist/components/ui/appointment-action-dialogs.js +1160 -0
  71. package/dist/components/ui/appointment-action-dialogs.mjs +23 -0
  72. package/dist/components/ui/appointment-availability-settings.js +1590 -0
  73. package/dist/components/ui/appointment-availability-settings.mjs +23 -0
  74. package/dist/components/ui/appointment-book-dialog.js +1744 -0
  75. package/dist/components/ui/appointment-book-dialog.mjs +27 -0
  76. package/dist/components/ui/appointment-calendar-view.js +833 -0
  77. package/dist/components/ui/appointment-calendar-view.mjs +14 -0
  78. package/dist/components/ui/appointment-detail-sheet.js +1517 -0
  79. package/dist/components/ui/appointment-detail-sheet.mjs +24 -0
  80. package/dist/components/ui/appointment-gmail-connect.js +467 -0
  81. package/dist/components/ui/appointment-gmail-connect.mjs +14 -0
  82. package/dist/components/ui/appointment-mini-card.js +345 -0
  83. package/dist/components/ui/appointment-mini-card.mjs +11 -0
  84. package/dist/components/ui/appointment-time-slot-picker.js +311 -0
  85. package/dist/components/ui/appointment-time-slot-picker.mjs +13 -0
  86. package/dist/components/ui/appointment-upcoming-card.js +1268 -0
  87. package/dist/components/ui/appointment-upcoming-card.mjs +21 -0
  88. package/dist/components/ui/backoffice-alert-history-chart.js +11 -5
  89. package/dist/components/ui/backoffice-alert-history-chart.mjs +5 -4
  90. package/dist/components/ui/backoffice-alerts-chart.js +786 -0
  91. package/dist/components/ui/backoffice-alerts-chart.mjs +19 -0
  92. package/dist/components/ui/backoffice-connections-chart.js +817 -0
  93. package/dist/components/ui/backoffice-connections-chart.mjs +19 -0
  94. package/dist/components/ui/backoffice-contact-history-chart.js +11 -5
  95. package/dist/components/ui/backoffice-contact-history-chart.mjs +5 -4
  96. package/dist/components/ui/badge.js +6 -6
  97. package/dist/components/ui/badge.mjs +1 -1
  98. package/dist/components/ui/borrowing-capacity-line-chart.js +30 -21
  99. package/dist/components/ui/borrowing-capacity-line-chart.mjs +5 -4
  100. package/dist/components/ui/button.js +2 -2
  101. package/dist/components/ui/button.mjs +1 -1
  102. package/dist/components/ui/calendar.js +2 -2
  103. package/dist/components/ui/calendar.mjs +2 -2
  104. package/dist/components/ui/card.js +1 -1
  105. package/dist/components/ui/card.mjs +1 -1
  106. package/dist/components/ui/cash-balance-line-chart.js +31 -23
  107. package/dist/components/ui/cash-balance-line-chart.mjs +5 -4
  108. package/dist/components/ui/cashflow-bar-chart.js +12 -5
  109. package/dist/components/ui/cashflow-bar-chart.mjs +5 -4
  110. package/dist/components/ui/chip.js +97 -18
  111. package/dist/components/ui/chip.mjs +3 -2
  112. package/dist/components/ui/color-picker.js +158 -28
  113. package/dist/components/ui/color-picker.mjs +3 -1
  114. package/dist/components/ui/data-table.js +140 -119
  115. package/dist/components/ui/data-table.mjs +3 -2
  116. package/dist/components/ui/date-picker.js +48 -27
  117. package/dist/components/ui/date-picker.mjs +4 -3
  118. package/dist/components/ui/dialog.js +37 -9
  119. package/dist/components/ui/dialog.mjs +2 -2
  120. package/dist/components/ui/expense-bar-chart.js +12 -5
  121. package/dist/components/ui/expense-bar-chart.mjs +5 -4
  122. package/dist/components/ui/field.mjs +2 -2
  123. package/dist/components/ui/financial-cards.js +322 -155
  124. package/dist/components/ui/financial-cards.mjs +5 -3
  125. package/dist/components/ui/financial-drawers.js +2 -2
  126. package/dist/components/ui/financial-drawers.mjs +3 -3
  127. package/dist/components/ui/financial-sections.js +14 -10
  128. package/dist/components/ui/financial-sections.mjs +6 -5
  129. package/dist/components/ui/income-bar-chart.js +12 -5
  130. package/dist/components/ui/income-bar-chart.mjs +5 -4
  131. package/dist/components/ui/input-group.js +2 -2
  132. package/dist/components/ui/input-group.mjs +2 -2
  133. package/dist/components/ui/kanban-column.js +52 -44
  134. package/dist/components/ui/kanban-column.mjs +7 -5
  135. package/dist/components/ui/opportunity-card.js +52 -44
  136. package/dist/components/ui/opportunity-card.mjs +6 -4
  137. package/dist/components/ui/opportunity-edit-modals.js +1367 -1263
  138. package/dist/components/ui/opportunity-edit-modals.mjs +8 -8
  139. package/dist/components/ui/opportunity-summary-tab.js +2744 -2157
  140. package/dist/components/ui/opportunity-summary-tab.mjs +14 -14
  141. package/dist/components/ui/page-header.js +92 -0
  142. package/dist/components/ui/page-header.mjs +8 -0
  143. package/dist/components/ui/page-top-bar.js +88 -0
  144. package/dist/components/ui/page-top-bar.mjs +8 -0
  145. package/dist/components/ui/pagination.js +303 -19
  146. package/dist/components/ui/pagination.mjs +11 -4
  147. package/dist/components/ui/pipeline-board.js +205 -191
  148. package/dist/components/ui/pipeline-board.mjs +9 -7
  149. package/dist/components/ui/pipeline-dialogs.js +114 -65
  150. package/dist/components/ui/pipeline-dialogs.mjs +7 -6
  151. package/dist/components/ui/pipeline-primitives.js +6 -6
  152. package/dist/components/ui/pipeline-primitives.mjs +2 -2
  153. package/dist/components/ui/property-cashflow-doughnut-chart.js +14 -12
  154. package/dist/components/ui/property-cashflow-doughnut-chart.mjs +5 -4
  155. package/dist/components/ui/property-debt-equity-doughnut-chart.js +14 -12
  156. package/dist/components/ui/property-debt-equity-doughnut-chart.mjs +5 -4
  157. package/dist/components/ui/property-mobile-estimate-line-chart.js +16 -14
  158. package/dist/components/ui/property-mobile-estimate-line-chart.mjs +5 -4
  159. package/dist/components/ui/sidebar-nav.js +426 -191
  160. package/dist/components/ui/sidebar-nav.mjs +5 -1
  161. package/dist/components/ui/stage-timeline.js +6 -6
  162. package/dist/components/ui/stage-timeline.mjs +3 -3
  163. package/dist/components/ui/transactions-expense-categories-doughnut-chart.js +18 -16
  164. package/dist/components/ui/transactions-expense-categories-doughnut-chart.mjs +5 -4
  165. package/dist/components/ui/transactions-income-expense-bar-chart.js +28 -12
  166. package/dist/components/ui/transactions-income-expense-bar-chart.mjs +5 -4
  167. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.js +18 -16
  168. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.mjs +5 -4
  169. package/dist/index.js +12258 -8611
  170. package/dist/index.mjs +258 -190
  171. package/dist/styles.css +1 -1
  172. package/package.json +71 -1
  173. package/src/components/index.tsx +115 -9
  174. package/src/components/ui/add-column-modal.tsx +7 -7
  175. package/src/components/ui/add-lead-modal.tsx +6 -3
  176. package/src/components/ui/advisor-card.tsx +284 -0
  177. package/src/components/ui/ai-assistant-drawer.tsx +4 -3
  178. package/src/components/ui/appointment-action-dialogs.tsx +297 -0
  179. package/src/components/ui/appointment-availability-settings.tsx +645 -0
  180. package/src/components/ui/appointment-book-dialog.tsx +618 -0
  181. package/src/components/ui/appointment-calendar-view.tsx +510 -0
  182. package/src/components/ui/appointment-detail-sheet.tsx +415 -0
  183. package/src/components/ui/appointment-gmail-connect.tsx +188 -0
  184. package/src/components/ui/appointment-mini-card.tsx +104 -0
  185. package/src/components/ui/appointment-time-slot-picker.tsx +123 -0
  186. package/src/components/ui/appointment-upcoming-card.tsx +635 -0
  187. package/src/components/ui/backoffice-alert-history-chart.tsx +10 -2
  188. package/src/components/ui/backoffice-alerts-chart.tsx +312 -0
  189. package/src/components/ui/backoffice-connections-chart.tsx +339 -0
  190. package/src/components/ui/backoffice-contact-history-chart.tsx +10 -2
  191. package/src/components/ui/badge.tsx +12 -6
  192. package/src/components/ui/borrowing-capacity-line-chart.tsx +4 -11
  193. package/src/components/ui/button.tsx +2 -2
  194. package/src/components/ui/card.tsx +1 -1
  195. package/src/components/ui/cash-balance-line-chart.tsx +4 -23
  196. package/src/components/ui/cashflow-bar-chart.tsx +9 -2
  197. package/src/components/ui/chart-shared.tsx +4 -11
  198. package/src/components/ui/chip.tsx +23 -19
  199. package/src/components/ui/color-picker.tsx +4 -2
  200. package/src/components/ui/data-table.tsx +28 -74
  201. package/src/components/ui/date-picker.tsx +42 -37
  202. package/src/components/ui/dialog.tsx +72 -6
  203. package/src/components/ui/expense-bar-chart.tsx +11 -2
  204. package/src/components/ui/financial-cards.tsx +99 -10
  205. package/src/components/ui/income-bar-chart.tsx +11 -2
  206. package/src/components/ui/opportunity-card.tsx +10 -39
  207. package/src/components/ui/opportunity-edit-modals.tsx +98 -36
  208. package/src/components/ui/opportunity-summary-tab.tsx +548 -232
  209. package/src/components/ui/page-header.tsx +57 -0
  210. package/src/components/ui/page-top-bar.tsx +48 -0
  211. package/src/components/ui/pagination.tsx +171 -22
  212. package/src/components/ui/pipeline-board.tsx +12 -5
  213. package/src/components/ui/property-cashflow-doughnut-chart.tsx +3 -1
  214. package/src/components/ui/property-debt-equity-doughnut-chart.tsx +3 -1
  215. package/src/components/ui/property-mobile-estimate-line-chart.tsx +3 -1
  216. package/src/components/ui/sidebar-nav.tsx +213 -157
  217. package/src/components/ui/transactions-expense-categories-doughnut-chart.tsx +3 -1
  218. package/src/components/ui/transactions-income-expense-bar-chart.tsx +12 -9
  219. package/src/components/ui/transactions-liabilities-breakdown-doughnut-chart.tsx +3 -1
  220. package/src/lib/format-currency.ts +44 -0
  221. package/src/lib/format-date.ts +50 -0
  222. package/src/lib/opportunity-constants.ts +12 -0
  223. package/src/styles/globals.css +17 -15
  224. package/src/styles/styles-css.ts +1 -1
  225. package/tsup.config.ts +14 -0
  226. package/dist/chunk-S4QRUQNW.mjs +0 -475
  227. package/dist/chunk-URGMJAE3.mjs +0 -1885
  228. package/dist/chunk-WNGWBVLV.mjs +0 -148
  229. package/dist/chunk-ZRSDX6OW.mjs +0 -385
  230. package/dist/{chunk-LLVQKSU3.mjs → chunk-GD4BJDJR.mjs} +3 -3
@@ -0,0 +1,312 @@
1
+ import React, { useMemo, useState } from "react";
2
+ import {
3
+ Chart as ChartJS,
4
+ CategoryScale,
5
+ LinearScale,
6
+ LineController,
7
+ LineElement,
8
+ PointElement,
9
+ BarController,
10
+ BarElement,
11
+ Tooltip,
12
+ type ChartOptions,
13
+ } from "chart.js";
14
+ import { Chart } from "react-chartjs-2";
15
+ import { BarChart2, LineChart } from "lucide-react";
16
+ import { useThemeVars } from "@/lib/theme-provider";
17
+ import { Card, CardContent, CardHeader, CardTitle, CardAction } from "./card";
18
+ import { Empty, EmptyDescription } from "./empty";
19
+ import { Skeleton } from "./skeleton";
20
+ import { cn } from "@/lib/utils";
21
+ import {
22
+ FALLBACK_TICK,
23
+ hexToRgba,
24
+ formatTooltipDate,
25
+ formatMonthLabel,
26
+ formatCount,
27
+ ChartLegendItem,
28
+ ChartPeriodButton,
29
+ } from "./chart-shared";
30
+ import { ToggleGroup, ToggleGroupItem } from "./toggle-group";
31
+
32
+ ChartJS.register(
33
+ CategoryScale,
34
+ LinearScale,
35
+ LineController,
36
+ LineElement,
37
+ PointElement,
38
+ BarController,
39
+ BarElement,
40
+ Tooltip,
41
+ );
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Types
45
+ // ---------------------------------------------------------------------------
46
+
47
+ export interface AlertSummaryDataPoint {
48
+ /** ISO date string e.g. "2024-01-01" */
49
+ date: string;
50
+ /** Count of Need Action alerts */
51
+ needAction: number;
52
+ /** Count of Watch alerts */
53
+ watch: number;
54
+ /** Count of Insight alerts */
55
+ insight: number;
56
+ }
57
+
58
+ export type AlertSummaryPeriod = 3 | 6 | 12;
59
+ type AlertChartType = "line" | "bar";
60
+
61
+ export interface BackofficeAlertsChartProps {
62
+ chartData?: AlertSummaryDataPoint[] | null;
63
+ title?: string;
64
+ showLegend?: boolean;
65
+ legendPosition?: "top" | "bottom";
66
+ showXAxis?: boolean;
67
+ showYAxis?: boolean;
68
+ defaultPeriod?: AlertSummaryPeriod;
69
+ height?: number;
70
+ width?: number | string;
71
+ className?: string;
72
+ isLoading?: boolean;
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Constants
77
+ // ---------------------------------------------------------------------------
78
+
79
+ const PERIODS: AlertSummaryPeriod[] = [3, 6, 12];
80
+ const SLICE_COUNT: Record<AlertSummaryPeriod, number> = { 3: 3, 6: 6, 12: 12 };
81
+
82
+ const NEED_ACTION_COLOR = "#F44336";
83
+ const WATCH_COLOR = "#FF9800";
84
+ const INSIGHT_COLOR = "#3B82F6";
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Legend
88
+ // ---------------------------------------------------------------------------
89
+
90
+ function AlertsLegend() {
91
+ return (
92
+ <div className="flex flex-wrap gap-x-3 gap-y-1.5 pb-2">
93
+ <ChartLegendItem
94
+ label="Need Action"
95
+ color={NEED_ACTION_COLOR}
96
+ lineStyle="solid"
97
+ />
98
+ <ChartLegendItem label="Watch" color={WATCH_COLOR} lineStyle="solid" />
99
+ <ChartLegendItem
100
+ label="Insight"
101
+ color={INSIGHT_COLOR}
102
+ lineStyle="solid"
103
+ />
104
+ </div>
105
+ );
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Component
110
+ // ---------------------------------------------------------------------------
111
+
112
+ export function BackofficeAlertsChart({
113
+ chartData,
114
+ title = "Alerts Summary",
115
+ showLegend = true,
116
+ legendPosition = "top",
117
+ showXAxis = true,
118
+ showYAxis = false,
119
+ defaultPeriod = 6,
120
+ height = 200,
121
+ width = "100%",
122
+ className,
123
+ isLoading = false,
124
+ }: BackofficeAlertsChartProps) {
125
+ const [period, setPeriod] = useState<AlertSummaryPeriod>(defaultPeriod);
126
+ const [chartType, setChartType] = useState<AlertChartType>("line");
127
+
128
+ const themeVars = useThemeVars();
129
+ const fontFamily: string =
130
+ (themeVars["--font-sans"] as string | undefined) || "Figtree, sans-serif";
131
+
132
+ const sliced = useMemo<AlertSummaryDataPoint[] | null>(() => {
133
+ if (!chartData?.length) return null;
134
+ const count = Math.min(SLICE_COUNT[period], chartData.length);
135
+ return chartData.slice(chartData.length - count);
136
+ }, [chartData, period]);
137
+
138
+ const labels = useMemo(
139
+ () => sliced?.map((p) => formatMonthLabel(p.date)) ?? [],
140
+ [sliced],
141
+ );
142
+
143
+ const isBar = chartType === "bar";
144
+
145
+ const datasets = useMemo(() => {
146
+ if (!sliced) return [];
147
+ return [
148
+ {
149
+ type: chartType,
150
+ label: "Need Action",
151
+ data: sliced.map((p) => p.needAction),
152
+ borderColor: NEED_ACTION_COLOR,
153
+ backgroundColor: isBar
154
+ ? NEED_ACTION_COLOR
155
+ : hexToRgba(NEED_ACTION_COLOR, 0.08),
156
+ fill: !isBar,
157
+ tension: 0.3,
158
+ pointRadius: 0,
159
+ borderWidth: 2,
160
+ },
161
+ {
162
+ type: chartType,
163
+ label: "Watch",
164
+ data: sliced.map((p) => p.watch),
165
+ borderColor: WATCH_COLOR,
166
+ backgroundColor: isBar ? WATCH_COLOR : hexToRgba(WATCH_COLOR, 0.08),
167
+ fill: !isBar,
168
+ tension: 0.3,
169
+ pointRadius: 0,
170
+ borderWidth: 2,
171
+ },
172
+ {
173
+ type: chartType,
174
+ label: "Insight",
175
+ data: sliced.map((p) => p.insight),
176
+ borderColor: INSIGHT_COLOR,
177
+ backgroundColor: isBar ? INSIGHT_COLOR : hexToRgba(INSIGHT_COLOR, 0.08),
178
+ fill: !isBar,
179
+ tension: 0.3,
180
+ pointRadius: 0,
181
+ borderWidth: 2,
182
+ },
183
+ ];
184
+ }, [sliced, chartType, isBar]);
185
+
186
+ const options = useMemo<ChartOptions<"bar">>(
187
+ () => ({
188
+ responsive: true,
189
+ maintainAspectRatio: false,
190
+ animation: { duration: 300 },
191
+ layout: { padding: 0 },
192
+ plugins: {
193
+ legend: { display: false },
194
+ tooltip: {
195
+ mode: "index",
196
+ intersect: false,
197
+ padding: 12,
198
+ cornerRadius: 0,
199
+ titleFont: { size: 11, weight: 600 },
200
+ bodyFont: { size: 12, weight: 500 },
201
+ callbacks: {
202
+ title: (tooltipItems) => {
203
+ const idx = tooltipItems[0]?.dataIndex;
204
+ if (idx != null && sliced?.[idx]?.date) {
205
+ return formatTooltipDate(sliced[idx].date, "monthly");
206
+ }
207
+ return tooltipItems[0]?.label ?? "";
208
+ },
209
+ label: (ctx) => {
210
+ const val = ctx.raw as number;
211
+ if (val === 0) return;
212
+ return ` ${ctx.dataset.label}: ${formatCount(val)}`;
213
+ },
214
+ },
215
+ },
216
+ },
217
+ scales: {
218
+ x: {
219
+ display: showXAxis,
220
+ grid: { display: false },
221
+ border: { display: false },
222
+ ticks: {
223
+ maxRotation: 0,
224
+ minRotation: 0,
225
+ color: FALLBACK_TICK,
226
+ font: { size: 10 },
227
+ },
228
+ },
229
+ y: {
230
+ display: showYAxis,
231
+ grid: { display: false },
232
+ border: { display: false },
233
+ beginAtZero: true,
234
+ ticks: {
235
+ padding: 8,
236
+ maxTicksLimit: 5,
237
+ color: FALLBACK_TICK,
238
+ font: { size: 10 },
239
+ callback: (v) => formatCount(Number(v)),
240
+ },
241
+ },
242
+ },
243
+ }),
244
+ [showXAxis, showYAxis, sliced],
245
+ );
246
+
247
+ return (
248
+ <Card
249
+ className={cn("w-full py-4 sm:py-6 gap-2", className)}
250
+ style={{ maxWidth: width, fontFamily }}
251
+ >
252
+ <CardHeader className="px-3 sm:px-6">
253
+ <CardTitle className="text-xs font-semibold uppercase tracking-wide">
254
+ {title}
255
+ </CardTitle>
256
+ <CardAction>
257
+ <div className="flex items-center gap-2">
258
+ <ToggleGroup
259
+ type="single"
260
+ variant="outline"
261
+ size="sm"
262
+ value={chartType}
263
+ onValueChange={(v) => v && setChartType(v as AlertChartType)}
264
+ >
265
+ <ToggleGroupItem value="line" aria-label="Line chart">
266
+ <LineChart className="size-3.5" />
267
+ </ToggleGroupItem>
268
+ <ToggleGroupItem value="bar" aria-label="Bar chart">
269
+ <BarChart2 className="size-3.5" />
270
+ </ToggleGroupItem>
271
+ </ToggleGroup>
272
+ <div className="flex gap-0.5 sm:gap-1">
273
+ {PERIODS.map((p) => (
274
+ <ChartPeriodButton
275
+ key={p}
276
+ period={p}
277
+ active={period === p}
278
+ onClick={() => setPeriod(p)}
279
+ />
280
+ ))}
281
+ </div>
282
+ </div>
283
+ </CardAction>
284
+ </CardHeader>
285
+
286
+ <CardContent className="px-3 sm:px-6">
287
+ {isLoading ? (
288
+ <Skeleton style={{ height, width: "100%" }} />
289
+ ) : !sliced ? (
290
+ <Empty className="flex-none p-4" style={{ height }}>
291
+ <EmptyDescription>No data available</EmptyDescription>
292
+ </Empty>
293
+ ) : (
294
+ <div className="flex flex-col gap-2">
295
+ {showLegend && legendPosition === "top" && <AlertsLegend />}
296
+ <div style={{ height, width: "100%", position: "relative" }}>
297
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
298
+ <Chart
299
+ key={chartType}
300
+ type={chartType as any}
301
+ data={{ labels, datasets } as any}
302
+ options={options}
303
+ aria-label={title}
304
+ />
305
+ </div>
306
+ {showLegend && legendPosition === "bottom" && <AlertsLegend />}
307
+ </div>
308
+ )}
309
+ </CardContent>
310
+ </Card>
311
+ );
312
+ }
@@ -0,0 +1,339 @@
1
+ import React, { useMemo, useState } from "react";
2
+ import {
3
+ Chart as ChartJS,
4
+ CategoryScale,
5
+ LinearScale,
6
+ LineController,
7
+ LineElement,
8
+ PointElement,
9
+ BarController,
10
+ BarElement,
11
+ Tooltip,
12
+ type ChartOptions,
13
+ } from "chart.js";
14
+ import { Chart } from "react-chartjs-2";
15
+ import { BarChart2, LineChart } from "lucide-react";
16
+ import { useThemeVars } from "@/lib/theme-provider";
17
+ import { Card, CardContent, CardHeader, CardTitle, CardAction } from "./card";
18
+ import { Empty, EmptyDescription } from "./empty";
19
+ import { Skeleton } from "./skeleton";
20
+ import { cn } from "@/lib/utils";
21
+ import {
22
+ FALLBACK_TICK,
23
+ hexToRgba,
24
+ formatTooltipDate,
25
+ formatMonthLabel,
26
+ formatCount,
27
+ ChartLegendItem,
28
+ ChartPeriodButton,
29
+ } from "./chart-shared";
30
+ import { ToggleGroup, ToggleGroupItem } from "./toggle-group";
31
+
32
+ ChartJS.register(
33
+ CategoryScale,
34
+ LinearScale,
35
+ LineController,
36
+ LineElement,
37
+ PointElement,
38
+ BarController,
39
+ BarElement,
40
+ Tooltip,
41
+ );
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Types
45
+ // ---------------------------------------------------------------------------
46
+
47
+ export interface ConnectionSummaryDataPoint {
48
+ /** ISO date string e.g. "2024-01-01" */
49
+ date: string;
50
+ /** Total contact count */
51
+ all: number;
52
+ /** Contacts with bank connected */
53
+ bankConnected: number;
54
+ /** Contacts with property connected */
55
+ propertyConnected: number;
56
+ /** Contacts with no connection */
57
+ notConnected: number;
58
+ }
59
+
60
+ export type ConnectionSummaryPeriod = 3 | 6 | 12;
61
+ type ConnectionChartType = "line" | "bar";
62
+
63
+ export interface BackofficeConnectionsChartProps {
64
+ chartData?: ConnectionSummaryDataPoint[] | null;
65
+ title?: string;
66
+ showLegend?: boolean;
67
+ legendPosition?: "top" | "bottom";
68
+ showXAxis?: boolean;
69
+ showYAxis?: boolean;
70
+ defaultPeriod?: ConnectionSummaryPeriod;
71
+ height?: number;
72
+ width?: number | string;
73
+ className?: string;
74
+ isLoading?: boolean;
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Constants
79
+ // ---------------------------------------------------------------------------
80
+
81
+ const PERIODS: ConnectionSummaryPeriod[] = [3, 6, 12];
82
+ const SLICE_COUNT: Record<ConnectionSummaryPeriod, number> = {
83
+ 3: 3,
84
+ 6: 6,
85
+ 12: 12,
86
+ };
87
+
88
+ const ALL_COLOR = "#040D13";
89
+ const BANK_COLOR = "#33FF99";
90
+ const PROPERTY_COLOR = "#3B82F6";
91
+ const NONE_COLOR = "#9EAAB5";
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Legend
95
+ // ---------------------------------------------------------------------------
96
+
97
+ function ConnectionsLegend() {
98
+ return (
99
+ <div className="flex flex-wrap gap-x-3 gap-y-1.5 pb-2">
100
+ <ChartLegendItem
101
+ label="All Contacts"
102
+ color={ALL_COLOR}
103
+ lineStyle="solid"
104
+ />
105
+ <ChartLegendItem
106
+ label="Bank Connected"
107
+ color={BANK_COLOR}
108
+ lineStyle="solid"
109
+ />
110
+ <ChartLegendItem
111
+ label="Property Connected"
112
+ color={PROPERTY_COLOR}
113
+ lineStyle="solid"
114
+ />
115
+ <ChartLegendItem
116
+ label="Not Connected"
117
+ color={NONE_COLOR}
118
+ lineStyle="solid"
119
+ />
120
+ </div>
121
+ );
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Component
126
+ // ---------------------------------------------------------------------------
127
+
128
+ export function BackofficeConnectionsChart({
129
+ chartData,
130
+ title = "Contacts and Connections",
131
+ showLegend = true,
132
+ legendPosition = "top",
133
+ showXAxis = true,
134
+ showYAxis = false,
135
+ defaultPeriod = 6,
136
+ height = 200,
137
+ width = "100%",
138
+ className,
139
+ isLoading = false,
140
+ }: BackofficeConnectionsChartProps) {
141
+ const [period, setPeriod] = useState<ConnectionSummaryPeriod>(defaultPeriod);
142
+ const [chartType, setChartType] = useState<ConnectionChartType>("line");
143
+
144
+ const themeVars = useThemeVars();
145
+ const fontFamily: string =
146
+ (themeVars["--font-sans"] as string | undefined) || "Figtree, sans-serif";
147
+
148
+ const sliced = useMemo<ConnectionSummaryDataPoint[] | null>(() => {
149
+ if (!chartData?.length) return null;
150
+ const count = Math.min(SLICE_COUNT[period], chartData.length);
151
+ return chartData.slice(chartData.length - count);
152
+ }, [chartData, period]);
153
+
154
+ const labels = useMemo(
155
+ () => sliced?.map((p) => formatMonthLabel(p.date)) ?? [],
156
+ [sliced],
157
+ );
158
+
159
+ const isBar = chartType === "bar";
160
+
161
+ const datasets = useMemo(() => {
162
+ if (!sliced) return [];
163
+ return [
164
+ {
165
+ type: chartType,
166
+ label: "All Contacts",
167
+ data: sliced.map((p) => p.all),
168
+ borderColor: ALL_COLOR,
169
+ backgroundColor: isBar ? ALL_COLOR : hexToRgba(ALL_COLOR, 0.08),
170
+ fill: !isBar,
171
+ tension: 0.3,
172
+ pointRadius: 0,
173
+ borderWidth: 2,
174
+ },
175
+ {
176
+ type: chartType,
177
+ label: "Bank Connected",
178
+ data: sliced.map((p) => p.bankConnected),
179
+ borderColor: BANK_COLOR,
180
+ backgroundColor: isBar ? BANK_COLOR : hexToRgba(BANK_COLOR, 0.08),
181
+ fill: !isBar,
182
+ tension: 0.3,
183
+ pointRadius: 0,
184
+ borderWidth: 2,
185
+ },
186
+ {
187
+ type: chartType,
188
+ label: "Property Connected",
189
+ data: sliced.map((p) => p.propertyConnected),
190
+ borderColor: PROPERTY_COLOR,
191
+ backgroundColor: isBar
192
+ ? PROPERTY_COLOR
193
+ : hexToRgba(PROPERTY_COLOR, 0.08),
194
+ fill: !isBar,
195
+ tension: 0.3,
196
+ pointRadius: 0,
197
+ borderWidth: 2,
198
+ },
199
+ {
200
+ type: chartType,
201
+ label: "Not Connected",
202
+ data: sliced.map((p) => p.notConnected),
203
+ borderColor: NONE_COLOR,
204
+ backgroundColor: isBar ? NONE_COLOR : hexToRgba(NONE_COLOR, 0.08),
205
+ fill: !isBar,
206
+ tension: 0.3,
207
+ pointRadius: 0,
208
+ borderWidth: 2,
209
+ },
210
+ ];
211
+ }, [sliced, chartType, isBar]);
212
+
213
+ const options = useMemo<ChartOptions<"bar">>(
214
+ () => ({
215
+ responsive: true,
216
+ maintainAspectRatio: false,
217
+ animation: { duration: 300 },
218
+ layout: { padding: 0 },
219
+ plugins: {
220
+ legend: { display: false },
221
+ tooltip: {
222
+ mode: "index",
223
+ intersect: false,
224
+ padding: 12,
225
+ cornerRadius: 0,
226
+ titleFont: { size: 11, weight: 600 },
227
+ bodyFont: { size: 12, weight: 500 },
228
+ callbacks: {
229
+ title: (tooltipItems) => {
230
+ const idx = tooltipItems[0]?.dataIndex;
231
+ if (idx != null && sliced?.[idx]?.date) {
232
+ return formatTooltipDate(sliced[idx].date, "monthly");
233
+ }
234
+ return tooltipItems[0]?.label ?? "";
235
+ },
236
+ label: (ctx) => {
237
+ const val = ctx.raw as number;
238
+ if (val === 0) return;
239
+ return ` ${ctx.dataset.label}: ${formatCount(val)}`;
240
+ },
241
+ },
242
+ },
243
+ },
244
+ scales: {
245
+ x: {
246
+ display: showXAxis,
247
+ grid: { display: false },
248
+ border: { display: false },
249
+ ticks: {
250
+ maxRotation: 0,
251
+ minRotation: 0,
252
+ color: FALLBACK_TICK,
253
+ font: { size: 10 },
254
+ },
255
+ },
256
+ y: {
257
+ display: showYAxis,
258
+ grid: { display: false },
259
+ border: { display: false },
260
+ beginAtZero: true,
261
+ ticks: {
262
+ padding: 8,
263
+ maxTicksLimit: 5,
264
+ color: FALLBACK_TICK,
265
+ font: { size: 10 },
266
+ callback: (v) => formatCount(Number(v)),
267
+ },
268
+ },
269
+ },
270
+ }),
271
+ [showXAxis, showYAxis, sliced],
272
+ );
273
+
274
+ return (
275
+ <Card
276
+ className={cn("w-full py-4 sm:py-6 gap-2", className)}
277
+ style={{ maxWidth: width, fontFamily }}
278
+ >
279
+ <CardHeader className="px-3 sm:px-6">
280
+ <CardTitle className="text-xs font-semibold uppercase tracking-wide">
281
+ {title}
282
+ </CardTitle>
283
+ <CardAction>
284
+ <div className="flex items-center gap-2">
285
+ <ToggleGroup
286
+ type="single"
287
+ variant="outline"
288
+ size="sm"
289
+ value={chartType}
290
+ onValueChange={(v) => v && setChartType(v as ConnectionChartType)}
291
+ >
292
+ <ToggleGroupItem value="line" aria-label="Line chart">
293
+ <LineChart className="size-3.5" />
294
+ </ToggleGroupItem>
295
+ <ToggleGroupItem value="bar" aria-label="Bar chart">
296
+ <BarChart2 className="size-3.5" />
297
+ </ToggleGroupItem>
298
+ </ToggleGroup>
299
+ <div className="flex gap-0.5 sm:gap-1">
300
+ {PERIODS.map((p) => (
301
+ <ChartPeriodButton
302
+ key={p}
303
+ period={p}
304
+ active={period === p}
305
+ onClick={() => setPeriod(p)}
306
+ />
307
+ ))}
308
+ </div>
309
+ </div>
310
+ </CardAction>
311
+ </CardHeader>
312
+
313
+ <CardContent className="px-3 sm:px-6">
314
+ {isLoading ? (
315
+ <Skeleton style={{ height, width: "100%" }} />
316
+ ) : !sliced ? (
317
+ <Empty className="flex-none p-4" style={{ height }}>
318
+ <EmptyDescription>No data available</EmptyDescription>
319
+ </Empty>
320
+ ) : (
321
+ <div className="flex flex-col gap-2">
322
+ {showLegend && legendPosition === "top" && <ConnectionsLegend />}
323
+ <div style={{ height, width: "100%", position: "relative" }}>
324
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
325
+ <Chart
326
+ key={chartType}
327
+ type={chartType as any}
328
+ data={{ labels, datasets } as any}
329
+ options={options}
330
+ aria-label={title}
331
+ />
332
+ </div>
333
+ {showLegend && legendPosition === "bottom" && <ConnectionsLegend />}
334
+ </div>
335
+ )}
336
+ </CardContent>
337
+ </Card>
338
+ );
339
+ }
@@ -26,7 +26,13 @@ import {
26
26
  ChartPeriodButton,
27
27
  } from "./chart-shared";
28
28
 
29
- ChartJS.register(CategoryScale, LinearScale, BarController, BarElement, Tooltip);
29
+ ChartJS.register(
30
+ CategoryScale,
31
+ LinearScale,
32
+ BarController,
33
+ BarElement,
34
+ Tooltip,
35
+ );
30
36
 
31
37
  // ---------------------------------------------------------------------------
32
38
  // Types
@@ -267,7 +273,9 @@ export function BackofficeContactHistoryChart({
267
273
  style={{ maxWidth: width, fontFamily }}
268
274
  >
269
275
  <CardHeader className="px-3 sm:px-6">
270
- <CardTitle className="text-sm sm:text-base">{title}</CardTitle>
276
+ <CardTitle className="text-xs font-semibold uppercase tracking-wide">
277
+ {title}
278
+ </CardTitle>
271
279
  <CardAction>
272
280
  <div className="flex gap-0.5 sm:gap-1">
273
281
  {CONTACT_PERIODS.map((p) => (
@@ -10,19 +10,25 @@ import { Slot } from "@/lib/slot";
10
10
  * Figma: Design-System---shadcn?node-id=665-2024
11
11
  *
12
12
  * WealthX: added font-sans. Extended with success/warning/info semantic variants.
13
+ * Semantic variants use a tinted outline style: bg-X/10 + border-X/40 + text-X-text.
14
+ * This gives softer, more readable badges and works correctly in light and dark mode.
13
15
  */
14
16
  const badgeVariants = cva(
15
17
  "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2.5 py-0.5 text-xs font-medium font-sans whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
16
18
  {
17
19
  variants: {
18
20
  variant: {
19
- default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
20
- secondary: "bg-muted text-muted-foreground [a&]:hover:bg-muted/80",
21
+ default:
22
+ "border-primary/40 bg-primary/10 text-foreground [a&]:hover:bg-primary/15",
23
+ secondary:
24
+ "border-border bg-muted text-muted-foreground [a&]:hover:bg-muted/80",
21
25
  destructive:
22
- "bg-destructive text-destructive-foreground focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
23
- success: "bg-success text-success-foreground [a&]:hover:bg-success/90",
24
- warning: "bg-warning text-warning-foreground [a&]:hover:bg-warning/90",
25
- info: "bg-info text-info-foreground [a&]:hover:bg-info/90",
26
+ "border-destructive/40 bg-destructive/10 text-destructive-text focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/15",
27
+ success:
28
+ "border-success/40 bg-success/10 text-success-text [a&]:hover:bg-success/15",
29
+ warning:
30
+ "border-warning/40 bg-warning/10 text-warning-text [a&]:hover:bg-warning/15",
31
+ info: "border-info/40 bg-info/10 text-info-text [a&]:hover:bg-info/15",
26
32
  outline:
27
33
  "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
28
34
  ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",