@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,635 @@
1
+ import React from "react";
2
+ import {
3
+ Calendar as CalendarIcon,
4
+ CalendarCheck,
5
+ Clock,
6
+ ExternalLink,
7
+ } from "lucide-react";
8
+ import { Avatar, AvatarFallback } from "./avatar";
9
+ import { Badge } from "./badge";
10
+ import { Button } from "./button";
11
+ import { Calendar as CalendarPicker } from "./calendar";
12
+ import {
13
+ Dialog,
14
+ DialogClose,
15
+ DialogContent,
16
+ DialogDescription,
17
+ DialogFooter,
18
+ DialogHeader,
19
+ DialogTitle,
20
+ } from "./dialog";
21
+ import { Label } from "./label";
22
+ import { Separator } from "./separator";
23
+ import { Textarea } from "./textarea";
24
+ import {
25
+ AppointmentSlotSection,
26
+ type AppointmentStatus,
27
+ type AppointmentTimeSlot,
28
+ } from "./appointment-time-slot-picker";
29
+
30
+ // Re-export so consumers who import these types from this module still work
31
+ export type { AppointmentStatus } from "./appointment-time-slot-picker";
32
+ export type { AppointmentTimeSlot as AppointmentUpcomingSlot } from "./appointment-time-slot-picker";
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Types
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /** @deprecated Use AppointmentStatus from appointment-time-slot-picker */
39
+ export type AppointmentUpcomingStatus = AppointmentStatus;
40
+
41
+ export interface AppointmentUpcomingAdvisor {
42
+ name: string;
43
+ role: string;
44
+ company: string;
45
+ avatarInitials: string;
46
+ }
47
+
48
+ export interface AppointmentUpcomingData {
49
+ status: AppointmentStatus;
50
+ advisor: AppointmentUpcomingAdvisor;
51
+ /** Human-readable appointment type — shown as the card header title */
52
+ appointmentType?: string;
53
+ /** Display date string — e.g. "2026-04-22" */
54
+ date: string;
55
+ /** Display start time — e.g. "10:00 AM" */
56
+ timeStart: string;
57
+ /** Display end time — e.g. "10:30 AM" */
58
+ timeEnd: string;
59
+ /** Optional Google Calendar / iCal link shown on Confirmed appointments */
60
+ calendarLink?: string;
61
+ /** Original date before advisor rescheduled — only set when status is "rescheduled" */
62
+ originalDate?: string;
63
+ originalTimeStart?: string;
64
+ originalTimeEnd?: string;
65
+ }
66
+
67
+ export interface AppointmentUpcomingCardProps {
68
+ /** Pass null or undefined to show the empty state with a Book CTA */
69
+ appointment?: AppointmentUpcomingData | null;
70
+ /** AM time slots shown in the reschedule dialog */
71
+ amSlots?: AppointmentTimeSlot[];
72
+ /** PM time slots shown in the reschedule dialog */
73
+ pmSlots?: AppointmentTimeSlot[];
74
+ onBookAppointment?: () => void;
75
+ /**
76
+ * Called when the client submits a reschedule request.
77
+ * @param date The preferred date the client chose.
78
+ * @param slot The preferred time slot (undefined when no slot data is provided).
79
+ * @param note Optional note from the client.
80
+ */
81
+ onReschedule?: (
82
+ date: Date,
83
+ slot: AppointmentTimeSlot | undefined,
84
+ note: string,
85
+ ) => void;
86
+ /**
87
+ * Called when the client confirms cancellation.
88
+ * @param reason Optional cancellation reason entered by the client.
89
+ */
90
+ onCancel?: (reason: string) => void;
91
+ onAcceptReschedule?: () => void;
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // AppointmentUpcomingCard
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /**
99
+ * Client-facing upcoming appointment card.
100
+ *
101
+ * Five states:
102
+ * - **Empty** — no appointment; prompts user to book
103
+ * - **Pending** — client booked, waiting for advisor confirmation
104
+ * - **Confirmed** — advisor confirmed; shows Add to Calendar if link provided
105
+ * - **Rescheduled** — advisor proposed a new time; Accept or Request Another Time
106
+ * - **Cancelled** — advisor cancelled; prompts user to rebook
107
+ */
108
+ export function AppointmentUpcomingCard({
109
+ appointment,
110
+ amSlots = [],
111
+ pmSlots = [],
112
+ onBookAppointment,
113
+ onReschedule,
114
+ onCancel,
115
+ onAcceptReschedule,
116
+ }: AppointmentUpcomingCardProps) {
117
+ // ── Dialog state ─────────────────────────────────────────────────────────────
118
+ const [calendarDialogOpen, setCalendarDialogOpen] = React.useState(false);
119
+ const [acceptDialogOpen, setAcceptDialogOpen] = React.useState(false);
120
+ const [rescheduleDialogOpen, setRescheduleDialogOpen] = React.useState(false);
121
+ const [cancelDialogOpen, setCancelDialogOpen] = React.useState(false);
122
+
123
+ // Reschedule form
124
+ const [rescheduleDate, setRescheduleDate] = React.useState<Date | undefined>(
125
+ new Date(),
126
+ );
127
+ const [rescheduleSlot, setRescheduleSlot] = React.useState<
128
+ AppointmentTimeSlot | undefined
129
+ >(undefined);
130
+ const [rescheduleNote, setRescheduleNote] = React.useState("");
131
+
132
+ // Cancel form
133
+ const [cancelReason, setCancelReason] = React.useState("");
134
+
135
+ const resetRescheduleForm = () => {
136
+ setRescheduleDate(new Date());
137
+ setRescheduleSlot(undefined);
138
+ setRescheduleNote("");
139
+ };
140
+
141
+ const hasSlotsData = amSlots.length > 0 || pmSlots.length > 0;
142
+ const canSubmitReschedule = hasSlotsData
143
+ ? !!rescheduleDate && !!rescheduleSlot
144
+ : !!rescheduleDate;
145
+
146
+ // ── Empty state ─────────────────────────────────────────────────────────────
147
+ if (!appointment) {
148
+ return (
149
+ <div className="flex flex-col items-center gap-4 border border-border bg-card px-6 py-10 text-center">
150
+ <div className="flex h-12 w-12 items-center justify-center bg-muted">
151
+ <CalendarIcon className="h-6 w-6 text-muted-foreground" />
152
+ </div>
153
+ <div className="flex flex-col gap-1.5">
154
+ <p className="text-base font-semibold">No upcoming appointments</p>
155
+ <p className="text-sm text-muted-foreground">
156
+ Schedule time with your advisor to discuss your financial goals.
157
+ </p>
158
+ </div>
159
+ <Button onClick={onBookAppointment} className="w-full">
160
+ Book an Appointment
161
+ </Button>
162
+ </div>
163
+ );
164
+ }
165
+
166
+ const isPending = appointment.status === "pending";
167
+ const isRescheduled = appointment.status === "rescheduled";
168
+ const isCancelled = appointment.status === "cancelled";
169
+
170
+ // ── Status badge ─────────────────────────────────────────────────────────────
171
+ const statusBadge = (() => {
172
+ if (isPending) return <Badge variant="warning">Pending</Badge>;
173
+ if (isRescheduled) return <Badge variant="info">Rescheduled</Badge>;
174
+ if (isCancelled) return <Badge variant="destructive">Cancelled</Badge>;
175
+ return <Badge variant="success">Confirmed</Badge>;
176
+ })();
177
+
178
+ // ── Status notice banner ──────────────────────────────────────────────────────
179
+ const noticeBanner = (() => {
180
+ if (isPending) {
181
+ return (
182
+ <div className="bg-warning/10 px-5 py-3">
183
+ <p className="text-sm text-foreground">
184
+ Your booking request has been sent. Your advisor will confirm the
185
+ appointment shortly.
186
+ </p>
187
+ </div>
188
+ );
189
+ }
190
+ if (isRescheduled) {
191
+ return (
192
+ <div className="bg-info/10 px-5 py-3">
193
+ <p className="text-sm text-foreground">
194
+ Your advisor has proposed a new time. Please review and respond
195
+ below.
196
+ </p>
197
+ </div>
198
+ );
199
+ }
200
+ if (isCancelled) {
201
+ return (
202
+ <div className="bg-destructive/10 px-5 py-3">
203
+ <p className="text-sm text-foreground">
204
+ This appointment has been cancelled by your advisor.
205
+ </p>
206
+ </div>
207
+ );
208
+ }
209
+ return null;
210
+ })();
211
+
212
+ const headerTitle = appointment.appointmentType
213
+ ? appointment.appointmentType.toUpperCase()
214
+ : "UPCOMING APPOINTMENT";
215
+
216
+ // ── Filled state ──────────────────────────────────────────────────────────────
217
+ return (
218
+ <>
219
+ <div className="flex flex-col border border-border bg-card">
220
+ {/* Header */}
221
+ <div className="flex items-center justify-between px-5 py-4">
222
+ <div className="flex items-center gap-2">
223
+ <CalendarCheck className="h-4 w-4 text-muted-foreground" />
224
+ <p className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
225
+ {headerTitle}
226
+ </p>
227
+ </div>
228
+ {statusBadge}
229
+ </div>
230
+
231
+ <Separator />
232
+
233
+ {/* Advisor info */}
234
+ <div className="flex items-center gap-4 px-5 py-4">
235
+ <Avatar className="h-12 w-12 shrink-0">
236
+ <AvatarFallback className="text-base">
237
+ {appointment.advisor.avatarInitials}
238
+ </AvatarFallback>
239
+ </Avatar>
240
+ <div className="flex flex-col gap-0.5">
241
+ <p className="text-base font-semibold">
242
+ {appointment.advisor.name}
243
+ </p>
244
+ <p className="text-sm text-muted-foreground">
245
+ {appointment.advisor.role}
246
+ </p>
247
+ <p className="text-xs text-muted-foreground">
248
+ {appointment.advisor.company}
249
+ </p>
250
+ </div>
251
+ </div>
252
+
253
+ <Separator />
254
+
255
+ {/* Date/time */}
256
+ <div className="flex flex-col gap-4 px-5 py-4">
257
+ {isRescheduled ? (
258
+ <>
259
+ <div className="flex flex-col gap-2">
260
+ <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
261
+ Original time
262
+ </p>
263
+ <div className="flex items-center gap-2.5 text-sm text-muted-foreground/50 line-through">
264
+ <CalendarIcon className="h-4 w-4 shrink-0" />
265
+ <span>{appointment.originalDate ?? appointment.date}</span>
266
+ </div>
267
+ <div className="flex items-center gap-2.5 text-sm text-muted-foreground/50 line-through">
268
+ <Clock className="h-4 w-4 shrink-0" />
269
+ <span>
270
+ {appointment.originalTimeStart ?? appointment.timeStart} –{" "}
271
+ {appointment.originalTimeEnd ?? appointment.timeEnd}
272
+ </span>
273
+ </div>
274
+ </div>
275
+ <div className="flex flex-col gap-2">
276
+ <p className="text-xs font-medium uppercase tracking-wide text-info">
277
+ New proposed time
278
+ </p>
279
+ <div className="flex items-center gap-2.5 text-sm text-foreground">
280
+ <CalendarIcon className="h-4 w-4 shrink-0" />
281
+ <span>{appointment.date}</span>
282
+ </div>
283
+ <div className="flex items-center gap-2.5 text-sm text-foreground">
284
+ <Clock className="h-4 w-4 shrink-0" />
285
+ <span>
286
+ {appointment.timeStart} – {appointment.timeEnd}
287
+ </span>
288
+ </div>
289
+ </div>
290
+ </>
291
+ ) : (
292
+ <>
293
+ <div
294
+ className={`flex items-center gap-2.5 text-sm ${
295
+ isCancelled
296
+ ? "text-muted-foreground/50 line-through"
297
+ : "text-muted-foreground"
298
+ }`}
299
+ >
300
+ <CalendarIcon className="h-4 w-4 shrink-0" />
301
+ <span>{appointment.date}</span>
302
+ </div>
303
+ <div
304
+ className={`flex items-center gap-2.5 text-sm ${
305
+ isCancelled
306
+ ? "text-muted-foreground/50 line-through"
307
+ : "text-muted-foreground"
308
+ }`}
309
+ >
310
+ <Clock className="h-4 w-4 shrink-0" />
311
+ <span>
312
+ {appointment.timeStart} – {appointment.timeEnd}
313
+ </span>
314
+ </div>
315
+ </>
316
+ )}
317
+ </div>
318
+
319
+ {/* Status notice */}
320
+ {noticeBanner && (
321
+ <>
322
+ <Separator />
323
+ {noticeBanner}
324
+ </>
325
+ )}
326
+
327
+ <Separator />
328
+
329
+ {/* Actions */}
330
+ <div className="flex flex-col gap-2 px-5 py-4">
331
+ {/* Confirmed — add to calendar */}
332
+ {!isPending &&
333
+ !isRescheduled &&
334
+ !isCancelled &&
335
+ appointment.calendarLink && (
336
+ <Button
337
+ className="w-full gap-2"
338
+ onClick={() => setCalendarDialogOpen(true)}
339
+ >
340
+ <ExternalLink className="h-4 w-4" />
341
+ Add to Calendar
342
+ </Button>
343
+ )}
344
+
345
+ {/* Rescheduled — accept or request another time */}
346
+ {isRescheduled && (
347
+ <>
348
+ <Button
349
+ className="w-full"
350
+ onClick={() => setAcceptDialogOpen(true)}
351
+ >
352
+ Accept New Time
353
+ </Button>
354
+ <Button
355
+ variant="outline"
356
+ className="w-full"
357
+ onClick={() => {
358
+ resetRescheduleForm();
359
+ setRescheduleDialogOpen(true);
360
+ }}
361
+ >
362
+ Request Another Time
363
+ </Button>
364
+ </>
365
+ )}
366
+
367
+ {/* Cancelled — rebook */}
368
+ {isCancelled && (
369
+ <Button className="w-full" onClick={onBookAppointment}>
370
+ Book a New Appointment
371
+ </Button>
372
+ )}
373
+
374
+ {/* Pending / Confirmed — reschedule + cancel */}
375
+ {!isRescheduled && !isCancelled && (
376
+ <>
377
+ <Button
378
+ variant="ghost"
379
+ className="w-full"
380
+ onClick={() => {
381
+ resetRescheduleForm();
382
+ setRescheduleDialogOpen(true);
383
+ }}
384
+ >
385
+ Request Reschedule
386
+ </Button>
387
+ <Button
388
+ variant="ghost"
389
+ className="w-full text-destructive hover:text-destructive"
390
+ onClick={() => {
391
+ setCancelReason("");
392
+ setCancelDialogOpen(true);
393
+ }}
394
+ >
395
+ Cancel Booking
396
+ </Button>
397
+ </>
398
+ )}
399
+ </div>
400
+ </div>
401
+
402
+ {/* ── Accept New Time dialog ────────────────────────────────────────── */}
403
+ <Dialog open={acceptDialogOpen} onOpenChange={setAcceptDialogOpen}>
404
+ <DialogContent>
405
+ <DialogHeader>
406
+ <DialogTitle>Accept New Time</DialogTitle>
407
+ <DialogDescription>
408
+ Confirm you want to accept{" "}
409
+ <span className="font-medium text-foreground">
410
+ {appointment.date}
411
+ </span>{" "}
412
+ from{" "}
413
+ <span className="font-medium text-foreground">
414
+ {appointment.timeStart} – {appointment.timeEnd}
415
+ </span>{" "}
416
+ as your new appointment time. Your advisor will be notified.
417
+ </DialogDescription>
418
+ </DialogHeader>
419
+ <DialogFooter>
420
+ <DialogClose render={<Button variant="outline" />}>
421
+ Keep Waiting
422
+ </DialogClose>
423
+ <Button
424
+ onClick={() => {
425
+ onAcceptReschedule?.();
426
+ setAcceptDialogOpen(false);
427
+ }}
428
+ >
429
+ Accept New Time
430
+ </Button>
431
+ </DialogFooter>
432
+ </DialogContent>
433
+ </Dialog>
434
+
435
+ {/* ── Add to Calendar dialog ─────────────────────────────────────────── */}
436
+ <Dialog open={calendarDialogOpen} onOpenChange={setCalendarDialogOpen}>
437
+ <DialogContent>
438
+ <DialogHeader>
439
+ <DialogTitle>Add to Google Calendar</DialogTitle>
440
+ <DialogDescription>
441
+ You&apos;ll be taken to Google Calendar to save this appointment.
442
+ Make sure you&apos;re signed in to the correct account.
443
+ </DialogDescription>
444
+ </DialogHeader>
445
+ <DialogFooter>
446
+ <DialogClose render={<Button variant="outline" />}>
447
+ Cancel
448
+ </DialogClose>
449
+ <Button
450
+ className="gap-1.5"
451
+ onClick={() => {
452
+ window.open(appointment.calendarLink, "_blank");
453
+ setCalendarDialogOpen(false);
454
+ }}
455
+ >
456
+ <ExternalLink className="h-4 w-4" />
457
+ Open Google Calendar
458
+ </Button>
459
+ </DialogFooter>
460
+ </DialogContent>
461
+ </Dialog>
462
+
463
+ {/* ── Request Reschedule dialog ──────────────────────────────────────── */}
464
+ <Dialog
465
+ open={rescheduleDialogOpen}
466
+ onOpenChange={(next) => {
467
+ if (!next) resetRescheduleForm();
468
+ setRescheduleDialogOpen(next);
469
+ }}
470
+ >
471
+ <DialogContent size="2xl">
472
+ <DialogHeader>
473
+ <DialogTitle>Request Reschedule</DialogTitle>
474
+ <DialogDescription>
475
+ Choose your preferred date and time. Your advisor will review the
476
+ request and confirm a new slot.
477
+ </DialogDescription>
478
+ </DialogHeader>
479
+
480
+ <Separator />
481
+
482
+ {/* Current booking */}
483
+ <div className="flex flex-col gap-1.5">
484
+ <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
485
+ Current booking
486
+ </p>
487
+ <div className="flex items-center gap-4 border border-border bg-muted/30 px-3 py-2.5 text-sm text-muted-foreground">
488
+ <span className="flex items-center gap-1.5">
489
+ <CalendarIcon className="h-3.5 w-3.5 shrink-0" />
490
+ {appointment.date}
491
+ </span>
492
+ <span className="flex items-center gap-1.5">
493
+ <Clock className="h-3.5 w-3.5 shrink-0" />
494
+ {appointment.timeStart} – {appointment.timeEnd}
495
+ </span>
496
+ </div>
497
+ </div>
498
+
499
+ <div className="grid grid-cols-[auto_1fr] items-start gap-5">
500
+ {/* Date picker */}
501
+ <div className="flex flex-col gap-1.5">
502
+ <Label>Preferred date</Label>
503
+ <CalendarPicker
504
+ mode="single"
505
+ selected={rescheduleDate}
506
+ onSelect={(d) => {
507
+ setRescheduleDate(d);
508
+ setRescheduleSlot(undefined);
509
+ }}
510
+ captionLayout="label"
511
+ fromDate={new Date()}
512
+ disabled={{ before: new Date() }}
513
+ className="border border-border"
514
+ />
515
+ </div>
516
+
517
+ {/* Time slots */}
518
+ <div className="flex flex-col gap-4">
519
+ {hasSlotsData ? (
520
+ rescheduleDate ? (
521
+ <>
522
+ <p className="text-sm font-semibold">Preferred time</p>
523
+ <AppointmentSlotSection
524
+ label="Morning"
525
+ slots={amSlots}
526
+ selectedSlotId={rescheduleSlot?.id}
527
+ onSelect={setRescheduleSlot}
528
+ />
529
+ <AppointmentSlotSection
530
+ label="Afternoon"
531
+ slots={pmSlots}
532
+ selectedSlotId={rescheduleSlot?.id}
533
+ onSelect={setRescheduleSlot}
534
+ />
535
+ </>
536
+ ) : (
537
+ <div className="flex h-full flex-col items-center justify-center gap-2 py-8 text-center">
538
+ <p className="text-sm font-semibold">Preferred time</p>
539
+ <p className="text-xs text-muted-foreground">
540
+ Pick a date on the left to see available slots.
541
+ </p>
542
+ </div>
543
+ )
544
+ ) : null}
545
+ </div>
546
+ </div>
547
+
548
+ {/* Note — full width */}
549
+ <div className="flex flex-col gap-1.5">
550
+ <Label htmlFor="reschedule-note">
551
+ Note{" "}
552
+ <span className="font-normal text-muted-foreground">
553
+ (optional)
554
+ </span>
555
+ </Label>
556
+ <Textarea
557
+ id="reschedule-note"
558
+ placeholder="e.g. I have a conflict at this time and would like to move to a later slot…"
559
+ value={rescheduleNote}
560
+ onChange={(e) => setRescheduleNote(e.target.value)}
561
+ className="w-full resize-none"
562
+ rows={2}
563
+ />
564
+ </div>
565
+
566
+ <DialogFooter>
567
+ <DialogClose render={<Button variant="outline" />}>
568
+ Cancel
569
+ </DialogClose>
570
+ <Button
571
+ disabled={!canSubmitReschedule}
572
+ onClick={() => {
573
+ onReschedule?.(rescheduleDate!, rescheduleSlot, rescheduleNote);
574
+ resetRescheduleForm();
575
+ setRescheduleDialogOpen(false);
576
+ }}
577
+ >
578
+ Submit Request
579
+ </Button>
580
+ </DialogFooter>
581
+ </DialogContent>
582
+ </Dialog>
583
+
584
+ {/* ── Cancel Booking dialog ──────────────────────────────────────────── */}
585
+ <Dialog
586
+ open={cancelDialogOpen}
587
+ onOpenChange={(next) => {
588
+ if (!next) setCancelReason("");
589
+ setCancelDialogOpen(next);
590
+ }}
591
+ >
592
+ <DialogContent>
593
+ <DialogHeader>
594
+ <DialogTitle>Cancel Booking</DialogTitle>
595
+ <DialogDescription>
596
+ Are you sure you want to cancel this appointment? Your advisor
597
+ will be notified. This action cannot be undone.
598
+ </DialogDescription>
599
+ </DialogHeader>
600
+ <div className="flex flex-col gap-1.5">
601
+ <Label htmlFor="cancel-reason">
602
+ Reason{" "}
603
+ <span className="font-normal text-muted-foreground">
604
+ (optional)
605
+ </span>
606
+ </Label>
607
+ <Textarea
608
+ id="cancel-reason"
609
+ placeholder="e.g. I need to reschedule due to a conflict…"
610
+ value={cancelReason}
611
+ onChange={(e) => setCancelReason(e.target.value)}
612
+ className="w-full resize-none"
613
+ rows={2}
614
+ />
615
+ </div>
616
+ <DialogFooter>
617
+ <DialogClose render={<Button variant="outline" />}>
618
+ Keep Booking
619
+ </DialogClose>
620
+ <Button
621
+ variant="destructive"
622
+ onClick={() => {
623
+ onCancel?.(cancelReason);
624
+ setCancelReason("");
625
+ setCancelDialogOpen(false);
626
+ }}
627
+ >
628
+ Cancel Booking
629
+ </Button>
630
+ </DialogFooter>
631
+ </DialogContent>
632
+ </Dialog>
633
+ </>
634
+ );
635
+ }
@@ -25,7 +25,13 @@ import {
25
25
  ChartPeriodButton,
26
26
  } from "./chart-shared";
27
27
 
28
- ChartJS.register(CategoryScale, LinearScale, BarController, BarElement, Tooltip);
28
+ ChartJS.register(
29
+ CategoryScale,
30
+ LinearScale,
31
+ BarController,
32
+ BarElement,
33
+ Tooltip,
34
+ );
29
35
 
30
36
  // ---------------------------------------------------------------------------
31
37
  // Types
@@ -219,7 +225,9 @@ export function BackofficeAlertHistoryChart({
219
225
  style={{ maxWidth: width, fontFamily }}
220
226
  >
221
227
  <CardHeader className="px-3 sm:px-6">
222
- <CardTitle className="text-sm sm:text-base">{title}</CardTitle>
228
+ <CardTitle className="text-xs font-semibold uppercase tracking-wide">
229
+ {title}
230
+ </CardTitle>
223
231
  <CardAction>
224
232
  <div className="flex gap-0.5 sm:gap-1">
225
233
  {ALERT_PERIODS.map((p) => (