@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,618 @@
1
+ import React from "react";
2
+ import { Button } from "./button";
3
+ import { Calendar as CalendarPicker } from "./calendar";
4
+ import {
5
+ Dialog,
6
+ DialogClose,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ } from "./dialog";
13
+ import { AddressAutocomplete } from "./form-primitives";
14
+ import { Label } from "./label";
15
+ import { RadioGroup, RadioGroupCard, RadioGroupItem } from "./radio-group";
16
+ import {
17
+ Select,
18
+ SelectContent,
19
+ SelectItem,
20
+ SelectTrigger,
21
+ SelectValue,
22
+ } from "./select";
23
+ import { Input } from "./input";
24
+ import { Separator } from "./separator";
25
+ import { Textarea } from "./textarea";
26
+ import { Toggle } from "./toggle";
27
+ import { Badge } from "./badge";
28
+ import { Avatar, AvatarFallback } from "./avatar";
29
+ import { MapPin, Phone, Video } from "lucide-react";
30
+ import {
31
+ AppointmentSlotSection,
32
+ type AppointmentMeetingFormat,
33
+ type AppointmentTimeSlot,
34
+ } from "./appointment-time-slot-picker";
35
+
36
+ // Re-export so consumers who import these types from this module still work
37
+ export type { AppointmentMeetingFormat } from "./appointment-time-slot-picker";
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Types
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export interface AppointmentClient {
44
+ id: string;
45
+ name: string;
46
+ email: string;
47
+ /** Phone numbers — for Call format. Multiple for Joint accounts. */
48
+ phones?: string[];
49
+ /** Account type — "Joint" shows multiple phones in Call format */
50
+ accountType?: "Individual" | "Joint";
51
+ }
52
+
53
+ /**
54
+ * Alias of AppointmentTimeSlot kept for backward compatibility.
55
+ * Prefer importing AppointmentTimeSlot from appointment-time-slot-picker.
56
+ */
57
+ export type AppointmentBookingSlot = AppointmentTimeSlot;
58
+
59
+ export type AppointmentOfflineLocation = "office" | "home" | "custom";
60
+
61
+ export interface AppointmentBookDialogProps {
62
+ open: boolean;
63
+ onOpenChange: (v: boolean) => void;
64
+ /**
65
+ * List of clients to search through.
66
+ *
67
+ * **Advisor mode (default):** provide a non-empty array — shows the client
68
+ * search field and "New appointment" header copy.
69
+ *
70
+ * **Client mode:** omit or pass an empty array — hides the client search,
71
+ * shows `advisorInfo` in the header instead, and changes the CTA copy to
72
+ * "Book Appointment".
73
+ */
74
+ clients?: AppointmentClient[];
75
+ /**
76
+ * Meeting type options. Omit or pass an empty array to hide the meeting
77
+ * type field (useful in client mode where the type is implicit).
78
+ */
79
+ meetingTypes?: string[];
80
+ amSlots: AppointmentTimeSlot[];
81
+ pmSlots: AppointmentTimeSlot[];
82
+ /**
83
+ * Advisor's weekly availability schedule. Days with `enabled: false` are
84
+ * disabled in the date picker so clients cannot select them.
85
+ */
86
+ schedule?: { day: string; enabled: boolean }[];
87
+ /**
88
+ * Advisor's office address pulled from company settings.
89
+ * Shown as an offline location option.
90
+ */
91
+ advisorOfficeAddress?: string;
92
+ /**
93
+ * Client's home address from their CRM profile.
94
+ * Shown as an offline location option when a client is selected.
95
+ */
96
+ clientHomeAddress?: string;
97
+ /**
98
+ * Advisor info shown in the dialog header when running in **client mode**
99
+ * (i.e. when `clients` is omitted or empty).
100
+ */
101
+ advisorInfo?: { name: string; role: string; initials: string };
102
+ /**
103
+ * Pre-select a client by ID when the dialog opens (advisor mode only).
104
+ * Used when rebooking from a cancelled appointment via `AppointmentDetailSheet`.
105
+ */
106
+ initialClientId?: string;
107
+ onBook?: (data: {
108
+ /** Empty string in client mode */
109
+ clientId: string;
110
+ /** Empty string when meetingTypes is omitted */
111
+ meetingType: string;
112
+ date: Date;
113
+ slot: AppointmentTimeSlot;
114
+ notes: string;
115
+ meetingFormat: AppointmentMeetingFormat;
116
+ offlineLocation?: AppointmentOfflineLocation;
117
+ customAddress?: string;
118
+ callPhone?: string;
119
+ }) => void;
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Client search sub-component
124
+ // ---------------------------------------------------------------------------
125
+
126
+ function ClientSearch({
127
+ clients,
128
+ value,
129
+ onValueChange,
130
+ }: {
131
+ clients: AppointmentClient[];
132
+ value: string | undefined;
133
+ onValueChange: (id: string | undefined) => void;
134
+ }) {
135
+ const [query, setQuery] = React.useState("");
136
+ const [open, setOpen] = React.useState(false);
137
+ const selected = clients.find((c) => c.id === value);
138
+ const filtered = clients.filter((c) => {
139
+ const q = query.toLowerCase();
140
+ return (
141
+ c.name.toLowerCase().includes(q) || c.email.toLowerCase().includes(q)
142
+ );
143
+ });
144
+
145
+ return (
146
+ <div className="relative">
147
+ <Input
148
+ value={selected ? selected.name : query}
149
+ onChange={(e) => {
150
+ setQuery(e.target.value);
151
+ if (selected) onValueChange(undefined);
152
+ setOpen(e.target.value.length > 0);
153
+ }}
154
+ onBlur={() => setTimeout(() => setOpen(false), 150)}
155
+ placeholder="Search by name or email…"
156
+ autoComplete="off"
157
+ />
158
+ {open && (filtered.length > 0 || query.length > 0) && (
159
+ <div className="absolute z-50 mt-1 w-full border border-border bg-popover shadow-md">
160
+ {filtered.length === 0 ? (
161
+ <p className="px-3 py-6 text-center text-sm text-muted-foreground">
162
+ No clients found.
163
+ </p>
164
+ ) : (
165
+ filtered.map((c) => (
166
+ <Button
167
+ key={c.id}
168
+ type="button"
169
+ variant="ghost"
170
+ className="h-auto w-full flex-col items-start gap-0.5 px-3 py-2 text-left hover:bg-primary/5"
171
+ onMouseDown={(e) => e.preventDefault()}
172
+ onClick={() => {
173
+ onValueChange(c.id);
174
+ setQuery("");
175
+ setOpen(false);
176
+ }}
177
+ >
178
+ <span className="text-sm font-medium">{c.name}</span>
179
+ <span className="text-xs text-muted-foreground">{c.email}</span>
180
+ </Button>
181
+ ))
182
+ )}
183
+ </div>
184
+ )}
185
+ </div>
186
+ );
187
+ }
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Meeting format sub-component
191
+ // ---------------------------------------------------------------------------
192
+
193
+ const FORMAT_OPTIONS: {
194
+ value: AppointmentMeetingFormat;
195
+ label: string;
196
+ icon: React.ReactNode;
197
+ }[] = [
198
+ { value: "call", label: "Call", icon: <Phone className="h-4 w-4" /> },
199
+ {
200
+ value: "google-meet",
201
+ label: "Google Meet",
202
+ icon: <Video className="h-4 w-4" />,
203
+ },
204
+ {
205
+ value: "offline",
206
+ label: "Offline",
207
+ icon: <MapPin className="h-4 w-4" />,
208
+ },
209
+ ];
210
+
211
+ function MeetingFormatSection({
212
+ format,
213
+ onFormatChange,
214
+ offlineLocation,
215
+ onOfflineLocationChange,
216
+ customAddress,
217
+ onCustomAddressChange,
218
+ advisorOfficeAddress,
219
+ clientHomeAddress,
220
+ isClientMode,
221
+ }: {
222
+ format: AppointmentMeetingFormat;
223
+ onFormatChange: (f: AppointmentMeetingFormat) => void;
224
+ offlineLocation: AppointmentOfflineLocation;
225
+ onOfflineLocationChange: (l: AppointmentOfflineLocation) => void;
226
+ customAddress: string;
227
+ onCustomAddressChange: (v: string) => void;
228
+ advisorOfficeAddress?: string;
229
+ clientHomeAddress?: string;
230
+ isClientMode?: boolean;
231
+ }) {
232
+ return (
233
+ <div className="flex flex-col gap-2">
234
+ <div className="flex gap-2">
235
+ {FORMAT_OPTIONS.map((opt) => (
236
+ <Toggle
237
+ key={opt.value}
238
+ variant="outline"
239
+ pressed={format === opt.value}
240
+ onPressedChange={() => onFormatChange(opt.value)}
241
+ className="flex-1 gap-1.5"
242
+ >
243
+ {opt.icon}
244
+ {opt.label}
245
+ </Toggle>
246
+ ))}
247
+ </div>
248
+
249
+ {format === "offline" && (
250
+ <div className="flex flex-col gap-2 border border-border p-3">
251
+ <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
252
+ Location
253
+ </p>
254
+ <RadioGroup
255
+ value={offlineLocation}
256
+ onValueChange={(v) =>
257
+ onOfflineLocationChange(v as AppointmentOfflineLocation)
258
+ }
259
+ className="gap-2"
260
+ >
261
+ {advisorOfficeAddress && (
262
+ <RadioGroupCard
263
+ value="office"
264
+ label="Advisor's Office"
265
+ description={advisorOfficeAddress}
266
+ />
267
+ )}
268
+ {clientHomeAddress && (
269
+ <RadioGroupCard
270
+ value="home"
271
+ label={isClientMode ? "Home" : "Client's Home"}
272
+ description={clientHomeAddress}
273
+ />
274
+ )}
275
+ <RadioGroupCard
276
+ value="custom"
277
+ label="Custom address"
278
+ description="Enter a specific location"
279
+ />
280
+ </RadioGroup>
281
+ {offlineLocation === "custom" && (
282
+ <AddressAutocomplete
283
+ value={customAddress}
284
+ onValueChange={onCustomAddressChange}
285
+ placeholder="e.g. 123 Collins St, Melbourne VIC 3000"
286
+ />
287
+ )}
288
+ </div>
289
+ )}
290
+ </div>
291
+ );
292
+ }
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // Main component
296
+ // ---------------------------------------------------------------------------
297
+
298
+ export function AppointmentBookDialog({
299
+ open,
300
+ onOpenChange,
301
+ clients = [],
302
+ meetingTypes = [],
303
+ amSlots,
304
+ pmSlots,
305
+ schedule,
306
+ advisorOfficeAddress,
307
+ clientHomeAddress,
308
+ advisorInfo,
309
+ initialClientId,
310
+ onBook,
311
+ }: AppointmentBookDialogProps) {
312
+ const isClientMode = clients.length === 0;
313
+
314
+ const DAY_MAP: Record<string, number> = {
315
+ Sun: 0,
316
+ Mon: 1,
317
+ Tue: 2,
318
+ Wed: 3,
319
+ Thu: 4,
320
+ Fri: 5,
321
+ Sat: 6,
322
+ };
323
+ const disabledDayOfWeek = schedule
324
+ ?.filter((d) => !d.enabled)
325
+ .map((d) => DAY_MAP[d.day])
326
+ .filter((n): n is number => n !== undefined);
327
+
328
+ const [clientId, setClientId] = React.useState<string | undefined>(undefined);
329
+ const [meetingType, setMeetingType] = React.useState("");
330
+ const [meetingFormat, setMeetingFormat] =
331
+ React.useState<AppointmentMeetingFormat>("call");
332
+ const [offlineLocation, setOfflineLocation] =
333
+ React.useState<AppointmentOfflineLocation>(
334
+ advisorOfficeAddress ? "office" : "custom",
335
+ );
336
+ const [customAddress, setCustomAddress] = React.useState("");
337
+ const [date, setDate] = React.useState<Date | undefined>(new Date());
338
+ const [selectedSlot, setSelectedSlot] = React.useState<
339
+ AppointmentTimeSlot | undefined
340
+ >(undefined);
341
+ const [notes, setNotes] = React.useState("");
342
+ const [selectedPhone, setSelectedPhone] = React.useState<string | undefined>(
343
+ undefined,
344
+ );
345
+
346
+ const selectedClient = clients.find((c) => c.id === clientId);
347
+
348
+ // Pre-select client when dialog opens with an initialClientId (e.g. rebook after cancellation)
349
+ React.useEffect(() => {
350
+ if (open && initialClientId) {
351
+ setClientId(initialClientId);
352
+ }
353
+ }, [open, initialClientId]);
354
+
355
+ React.useEffect(() => {
356
+ setSelectedPhone(undefined);
357
+ }, [clientId]);
358
+
359
+ React.useEffect(() => {
360
+ if (meetingFormat !== "call") {
361
+ setSelectedPhone(undefined);
362
+ }
363
+ }, [meetingFormat]);
364
+
365
+ const clientReady = isClientMode || !!clientId;
366
+ const meetingTypeReady = meetingTypes.length === 0 || !!meetingType;
367
+ const offlineReady =
368
+ meetingFormat !== "offline" ||
369
+ offlineLocation !== "custom" ||
370
+ !!customAddress.trim();
371
+
372
+ const canSubmit =
373
+ clientReady && meetingTypeReady && !!date && !!selectedSlot && offlineReady;
374
+
375
+ const handleOpenChange = (next: boolean) => {
376
+ if (!next) {
377
+ setClientId(undefined);
378
+ setMeetingType("");
379
+ setMeetingFormat("call");
380
+ setOfflineLocation(advisorOfficeAddress ? "office" : "custom");
381
+ setCustomAddress("");
382
+ setDate(new Date());
383
+ setSelectedSlot(undefined);
384
+ setNotes("");
385
+ setSelectedPhone(undefined);
386
+ }
387
+ onOpenChange(next);
388
+ };
389
+
390
+ return (
391
+ <Dialog open={open} onOpenChange={handleOpenChange}>
392
+ <DialogContent size="2xl" align="top">
393
+ <DialogHeader>
394
+ <DialogTitle>
395
+ {isClientMode ? "Book an Appointment" : "New appointment"}
396
+ </DialogTitle>
397
+ <DialogDescription>
398
+ {isClientMode
399
+ ? "Request a meeting with your advisor. They will confirm once the appointment is reviewed."
400
+ : "Book a meeting with a client. They will receive a confirmation email once the appointment is created."}
401
+ </DialogDescription>
402
+
403
+ {/* Advisor chip — client mode only */}
404
+ {isClientMode && advisorInfo && (
405
+ <div className="mt-3 flex items-center gap-3 border border-border bg-muted/30 px-3 py-2">
406
+ <Avatar className="h-8 w-8 shrink-0">
407
+ <AvatarFallback className="text-xs">
408
+ {advisorInfo.initials}
409
+ </AvatarFallback>
410
+ </Avatar>
411
+ <div className="flex flex-col gap-0.5">
412
+ <p className="text-sm font-medium">{advisorInfo.name}</p>
413
+ <p className="text-xs text-muted-foreground">
414
+ {advisorInfo.role}
415
+ </p>
416
+ </div>
417
+ </div>
418
+ )}
419
+ </DialogHeader>
420
+
421
+ <Separator />
422
+
423
+ <div className="flex flex-col gap-4">
424
+ {/* Client search — advisor mode only */}
425
+ {!isClientMode && (
426
+ <div className="flex flex-col gap-1.5">
427
+ <Label>Client</Label>
428
+ <ClientSearch
429
+ clients={clients}
430
+ value={clientId}
431
+ onValueChange={setClientId}
432
+ />
433
+ </div>
434
+ )}
435
+
436
+ {/* Appointment type — shown only when meetingTypes provided */}
437
+ {meetingTypes.length > 0 && (
438
+ <div className="flex flex-col gap-1.5">
439
+ <Label htmlFor="book-apt-type">
440
+ {isClientMode ? "Appointment type" : "Meeting type"}
441
+ </Label>
442
+ <Select onValueChange={(v) => setMeetingType(v as string)}>
443
+ <SelectTrigger id="book-apt-type" className="w-full">
444
+ <SelectValue placeholder="Select meeting type…" />
445
+ </SelectTrigger>
446
+ <SelectContent>
447
+ {meetingTypes.map((t) => (
448
+ <SelectItem key={t} value={t}>
449
+ {t}
450
+ </SelectItem>
451
+ ))}
452
+ </SelectContent>
453
+ </Select>
454
+ </div>
455
+ )}
456
+
457
+ {/* Meeting format */}
458
+ <div className="flex flex-col gap-1.5">
459
+ <Label>Meeting format</Label>
460
+ <MeetingFormatSection
461
+ format={meetingFormat}
462
+ onFormatChange={setMeetingFormat}
463
+ offlineLocation={offlineLocation}
464
+ onOfflineLocationChange={setOfflineLocation}
465
+ customAddress={customAddress}
466
+ onCustomAddressChange={setCustomAddress}
467
+ advisorOfficeAddress={advisorOfficeAddress}
468
+ clientHomeAddress={clientHomeAddress}
469
+ isClientMode={isClientMode}
470
+ />
471
+ </div>
472
+
473
+ {/* Phone selection — advisor mode, Call format, client has phones */}
474
+ {!isClientMode &&
475
+ meetingFormat === "call" &&
476
+ selectedClient?.phones &&
477
+ selectedClient.phones.length > 0 && (
478
+ <div className="flex flex-col gap-1.5">
479
+ <Label>
480
+ {selectedClient.accountType === "Joint"
481
+ ? "Select a contact number"
482
+ : "Phone number"}
483
+ </Label>
484
+ <RadioGroup
485
+ value={selectedPhone ?? ""}
486
+ onValueChange={(v) => setSelectedPhone(v as string)}
487
+ className="gap-1 border border-border p-3"
488
+ >
489
+ {selectedClient.phones.map((phone) => (
490
+ <div
491
+ key={phone}
492
+ className="flex items-center gap-3 p-2 hover:bg-muted/50"
493
+ >
494
+ <RadioGroupItem value={phone} id={`phone-${phone}`} />
495
+ <Label
496
+ htmlFor={`phone-${phone}`}
497
+ className="flex cursor-pointer items-center gap-2 font-normal"
498
+ >
499
+ <Phone className="h-4 w-4 text-muted-foreground" />
500
+ {phone}
501
+ </Label>
502
+ </div>
503
+ ))}
504
+ </RadioGroup>
505
+ </div>
506
+ )}
507
+
508
+ {/* Date + Time slot — side by side */}
509
+ <div className="grid grid-cols-[auto_1fr] items-start gap-5">
510
+ <div className="flex flex-col gap-1.5">
511
+ <Label>Date</Label>
512
+ <CalendarPicker
513
+ mode="single"
514
+ selected={date}
515
+ onSelect={(d) => {
516
+ setDate(d);
517
+ setSelectedSlot(undefined);
518
+ }}
519
+ captionLayout="label"
520
+ fromDate={new Date()}
521
+ disabled={
522
+ disabledDayOfWeek && disabledDayOfWeek.length > 0
523
+ ? [{ before: new Date() }, { dayOfWeek: disabledDayOfWeek }]
524
+ : { before: new Date() }
525
+ }
526
+ className="border border-border"
527
+ />
528
+ </div>
529
+ <div className="flex flex-col gap-1.5">
530
+ <Label>Time slot</Label>
531
+ {date ? (
532
+ <div className="flex flex-col gap-3">
533
+ <p className="text-xs text-muted-foreground">
534
+ {date.toLocaleDateString("en-AU", {
535
+ weekday: "long",
536
+ day: "numeric",
537
+ month: "long",
538
+ })}
539
+ </p>
540
+ <AppointmentSlotSection
541
+ label="Morning"
542
+ slots={amSlots}
543
+ selectedSlotId={selectedSlot?.id}
544
+ onSelect={setSelectedSlot}
545
+ />
546
+ <AppointmentSlotSection
547
+ label="Afternoon"
548
+ slots={pmSlots}
549
+ selectedSlotId={selectedSlot?.id}
550
+ onSelect={setSelectedSlot}
551
+ />
552
+ </div>
553
+ ) : (
554
+ <p className="text-sm text-muted-foreground">
555
+ Select a date to see available slots.
556
+ </p>
557
+ )}
558
+ </div>
559
+ </div>
560
+
561
+ {/* Notes */}
562
+ <div className="flex flex-col gap-1.5">
563
+ <Label htmlFor="book-apt-notes">
564
+ Notes{" "}
565
+ <span className="font-normal text-muted-foreground">
566
+ (optional)
567
+ </span>
568
+ </Label>
569
+ <Textarea
570
+ id="book-apt-notes"
571
+ placeholder={
572
+ isClientMode
573
+ ? "Let your advisor know what you'd like to discuss…"
574
+ : "e.g. Client wants to discuss refinancing their investment property…"
575
+ }
576
+ value={notes}
577
+ onChange={(e) => setNotes(e.target.value)}
578
+ className="w-full resize-none"
579
+ rows={2}
580
+ />
581
+ </div>
582
+ </div>
583
+
584
+ <DialogFooter>
585
+ <DialogClose render={<Button variant="outline" />}>
586
+ Cancel
587
+ </DialogClose>
588
+ <Button
589
+ disabled={!canSubmit}
590
+ onClick={() => {
591
+ if (canSubmit && date && selectedSlot) {
592
+ onBook?.({
593
+ clientId: clientId ?? "",
594
+ meetingType,
595
+ date,
596
+ slot: selectedSlot,
597
+ notes,
598
+ meetingFormat,
599
+ offlineLocation:
600
+ meetingFormat === "offline" ? offlineLocation : undefined,
601
+ customAddress:
602
+ meetingFormat === "offline" && offlineLocation === "custom"
603
+ ? customAddress
604
+ : undefined,
605
+ callPhone:
606
+ meetingFormat === "call" ? selectedPhone : undefined,
607
+ });
608
+ handleOpenChange(false);
609
+ }
610
+ }}
611
+ >
612
+ {isClientMode ? "Book Appointment" : "Create appointment"}
613
+ </Button>
614
+ </DialogFooter>
615
+ </DialogContent>
616
+ </Dialog>
617
+ );
618
+ }