@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,645 @@
1
+ import React from "react";
2
+ import { Button } from "./button";
3
+ import { DatePicker } from "./date-picker";
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ } from "./dialog";
12
+ import { Input } from "./input";
13
+ import { Label } from "./label";
14
+ import {
15
+ Select,
16
+ SelectContent,
17
+ SelectItem,
18
+ SelectTrigger,
19
+ SelectValue,
20
+ } from "./select";
21
+ import { Separator } from "./separator";
22
+ import { Switch } from "./switch";
23
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
24
+ import { cn } from "@/lib/utils";
25
+ import { formatDateWithWeekday } from "@/lib/format-date";
26
+ import { Plus, X } from "lucide-react";
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Types
30
+ // ---------------------------------------------------------------------------
31
+
32
+ export interface AppointmentDaySchedule {
33
+ day: "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" | "Sun";
34
+ enabled: boolean;
35
+ /** 24h format — "09:00" */
36
+ startTime: string;
37
+ /** 24h format — "17:00" */
38
+ endTime: string;
39
+ }
40
+
41
+ export interface AppointmentAvailabilityPrefs {
42
+ meetingDuration: string;
43
+ schedulingBuffer: string;
44
+ maxSlotsPerDay: string;
45
+ }
46
+
47
+ export interface AppointmentBlockedDate {
48
+ /** ISO date string — "2026-04-25" */
49
+ date: string;
50
+ /** Human-readable label — "ANZAC Day" */
51
+ label?: string;
52
+ /** Partial day — start time in 24h format "09:00" */
53
+ timeStart?: string;
54
+ /** Partial day — end time in 24h format "17:00" */
55
+ timeEnd?: string;
56
+ }
57
+
58
+ export interface AppointmentAvailabilitySettingsProps {
59
+ schedule: AppointmentDaySchedule[];
60
+ blockedDates?: AppointmentBlockedDate[];
61
+ onSave?: (
62
+ schedule: AppointmentDaySchedule[],
63
+ prefs: AppointmentAvailabilityPrefs,
64
+ ) => void;
65
+ onBlockedDatesChange?: (dates: AppointmentBlockedDate[]) => void;
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // AU Public Holidays 2026
70
+ // ---------------------------------------------------------------------------
71
+
72
+ const AU_PUBLIC_HOLIDAYS_2026: AppointmentBlockedDate[] = [
73
+ { date: "2026-01-01", label: "New Year's Day" },
74
+ { date: "2026-01-26", label: "Australia Day" },
75
+ { date: "2026-04-03", label: "Good Friday" },
76
+ { date: "2026-04-04", label: "Easter Saturday" },
77
+ { date: "2026-04-06", label: "Easter Monday" },
78
+ { date: "2026-04-25", label: "ANZAC Day" },
79
+ { date: "2026-06-08", label: "King's Birthday" },
80
+ { date: "2026-12-25", label: "Christmas Day" },
81
+ { date: "2026-12-26", label: "Boxing Day" },
82
+ ];
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Internal type — Time Off entries with switch state
86
+ // ---------------------------------------------------------------------------
87
+
88
+ interface TimeOffEntry {
89
+ date: string;
90
+ label?: string;
91
+ enabled: boolean;
92
+ isHoliday: boolean;
93
+ timeStart?: string;
94
+ timeEnd?: string;
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Time options — 30-min increments from 6:00 to 21:30
99
+ // ---------------------------------------------------------------------------
100
+
101
+ const TIME_OPTIONS: { value: string; label: string }[] = (() => {
102
+ const opts: { value: string; label: string }[] = [];
103
+ for (let h = 6; h <= 21; h++) {
104
+ for (const m of [0, 30]) {
105
+ const hh = String(h).padStart(2, "0");
106
+ const mm = String(m).padStart(2, "0");
107
+ const hour12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
108
+ const ampm = h < 12 ? "AM" : "PM";
109
+ opts.push({ value: `${hh}:${mm}`, label: `${hour12}:${mm} ${ampm}` });
110
+ }
111
+ }
112
+ return opts;
113
+ })();
114
+
115
+ const timeLabel = (v: string) =>
116
+ TIME_OPTIONS.find((o) => o.value === v)?.label ?? v;
117
+
118
+ function TimeSelect({
119
+ value,
120
+ onChange,
121
+ disabled,
122
+ }: {
123
+ value: string;
124
+ onChange: (v: string) => void;
125
+ disabled?: boolean;
126
+ }) {
127
+ return (
128
+ <Select
129
+ value={value}
130
+ onValueChange={(v) => onChange(v as string)}
131
+ disabled={disabled}
132
+ >
133
+ <SelectTrigger className="w-36">
134
+ <SelectValue />
135
+ </SelectTrigger>
136
+ <SelectContent>
137
+ {TIME_OPTIONS.map((opt) => (
138
+ <SelectItem key={opt.value} value={opt.value}>
139
+ {opt.label}
140
+ </SelectItem>
141
+ ))}
142
+ </SelectContent>
143
+ </Select>
144
+ );
145
+ }
146
+
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // "Add More" dialog
150
+ // ---------------------------------------------------------------------------
151
+
152
+ function AddTimeOffDialog({
153
+ open,
154
+ onOpenChange,
155
+ onAdd,
156
+ }: {
157
+ open: boolean;
158
+ onOpenChange: (v: boolean) => void;
159
+ onAdd: (entry: {
160
+ date: string;
161
+ label?: string;
162
+ timeStart?: string;
163
+ timeEnd?: string;
164
+ }) => void;
165
+ }) {
166
+ const [label, setLabel] = React.useState("");
167
+ const [date, setDate] = React.useState<Date | undefined>(undefined);
168
+ const [includeTime, setIncludeTime] = React.useState(false);
169
+ const [timeStart, setTimeStart] = React.useState("09:00");
170
+ const [timeEnd, setTimeEnd] = React.useState("17:00");
171
+
172
+ const reset = () => {
173
+ setLabel("");
174
+ setDate(undefined);
175
+ setIncludeTime(false);
176
+ setTimeStart("09:00");
177
+ setTimeEnd("17:00");
178
+ };
179
+
180
+ const handleAdd = () => {
181
+ if (!date) return;
182
+ // Convert Date to ISO date string "YYYY-MM-DD"
183
+ const isoDate = [
184
+ date.getFullYear(),
185
+ String(date.getMonth() + 1).padStart(2, "0"),
186
+ String(date.getDate()).padStart(2, "0"),
187
+ ].join("-");
188
+ onAdd({
189
+ date: isoDate,
190
+ label: label.trim() || undefined,
191
+ timeStart: includeTime ? timeStart : undefined,
192
+ timeEnd: includeTime ? timeEnd : undefined,
193
+ });
194
+ reset();
195
+ onOpenChange(false);
196
+ };
197
+
198
+ const handleCancel = () => {
199
+ reset();
200
+ onOpenChange(false);
201
+ };
202
+
203
+ return (
204
+ <Dialog open={open} onOpenChange={onOpenChange}>
205
+ <DialogContent size="auto" minWidth="26rem" align="top">
206
+ <DialogHeader>
207
+ <DialogTitle>Add time off</DialogTitle>
208
+ <DialogDescription>
209
+ Block a date when you are unavailable. Clients cannot book on this
210
+ date.
211
+ </DialogDescription>
212
+ </DialogHeader>
213
+
214
+ <div className="flex flex-col gap-4 py-1">
215
+ {/* Label */}
216
+ <div className="flex flex-col gap-1.5">
217
+ <Label htmlFor="toff-label">
218
+ Name{" "}
219
+ <span className="font-normal text-muted-foreground">
220
+ (optional)
221
+ </span>
222
+ </Label>
223
+ <Input
224
+ id="toff-label"
225
+ placeholder="e.g. Conference, Personal day…"
226
+ value={label}
227
+ onChange={(e) => setLabel(e.target.value)}
228
+ />
229
+ </div>
230
+
231
+ {/* Date */}
232
+ <div className="flex flex-col gap-1.5">
233
+ <Label>Date</Label>
234
+ <DatePicker
235
+ value={date}
236
+ onChange={setDate}
237
+ placeholder="Pick a date"
238
+ calendarProps={{ fromDate: new Date() }}
239
+ />
240
+ </div>
241
+
242
+ {/* Add time switch */}
243
+ <div className="flex flex-col gap-3">
244
+ <div className="flex items-center gap-3">
245
+ <Switch
246
+ id="toff-include-time"
247
+ checked={includeTime}
248
+ onCheckedChange={setIncludeTime}
249
+ />
250
+ <Label
251
+ htmlFor="toff-include-time"
252
+ className="cursor-pointer text-sm"
253
+ >
254
+ Specify hours (partial day)
255
+ </Label>
256
+ </div>
257
+
258
+ {includeTime && (
259
+ <div className="flex items-center gap-3 pl-10">
260
+ <TimeSelect value={timeStart} onChange={setTimeStart} />
261
+ <span className="text-sm text-muted-foreground">to</span>
262
+ <TimeSelect value={timeEnd} onChange={setTimeEnd} />
263
+ </div>
264
+ )}
265
+ </div>
266
+ </div>
267
+
268
+ <DialogFooter>
269
+ <Button variant="outline" onClick={handleCancel}>
270
+ Cancel
271
+ </Button>
272
+ <Button onClick={handleAdd} disabled={!date}>
273
+ Add
274
+ </Button>
275
+ </DialogFooter>
276
+ </DialogContent>
277
+ </Dialog>
278
+ );
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // Component
283
+ // ---------------------------------------------------------------------------
284
+
285
+ export function AppointmentAvailabilitySettings({
286
+ schedule: initialSchedule,
287
+ blockedDates: blockedDatesProp,
288
+ onSave,
289
+ onBlockedDatesChange,
290
+ }: AppointmentAvailabilitySettingsProps) {
291
+ // Track first render to skip auto-save on mount
292
+ const hasMounted = React.useRef(false);
293
+ React.useEffect(() => {
294
+ hasMounted.current = true;
295
+ }, []);
296
+
297
+ // --- Weekly Availability tab ---
298
+ const [schedule, setSchedule] =
299
+ React.useState<AppointmentDaySchedule[]>(initialSchedule);
300
+
301
+ // --- Booking Preferences tab ---
302
+ const [meetingDuration, setMeetingDuration] = React.useState("30");
303
+ const [schedulingBuffer, setSchedulingBuffer] = React.useState("0");
304
+ const [maxSlotsPerDay, setMaxSlotsPerDay] = React.useState("8");
305
+
306
+ // --- Time Off tab ---
307
+ const [timeOffEntries, setTimeOffEntries] = React.useState<TimeOffEntry[]>(
308
+ () => {
309
+ const holidayDates = new Set(AU_PUBLIC_HOLIDAYS_2026.map((h) => h.date));
310
+ const entries: TimeOffEntry[] = AU_PUBLIC_HOLIDAYS_2026.map((h) => ({
311
+ ...h,
312
+ enabled: true,
313
+ isHoliday: true,
314
+ }));
315
+ blockedDatesProp?.forEach((b) => {
316
+ if (!holidayDates.has(b.date)) {
317
+ entries.push({
318
+ date: b.date,
319
+ label: b.label,
320
+ enabled: true,
321
+ isHoliday: false,
322
+ });
323
+ }
324
+ });
325
+ return entries.sort((a, b) => a.date.localeCompare(b.date));
326
+ },
327
+ );
328
+ const [addMoreOpen, setAddMoreOpen] = React.useState(false);
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Auto-save — fires after each deliberate state change (skip initial mount)
332
+ // ---------------------------------------------------------------------------
333
+
334
+ const currentPrefs = React.useMemo(
335
+ () => ({ meetingDuration, schedulingBuffer, maxSlotsPerDay }),
336
+ [meetingDuration, schedulingBuffer, maxSlotsPerDay],
337
+ );
338
+
339
+ React.useEffect(() => {
340
+ if (!hasMounted.current) return;
341
+ onSave?.(schedule, currentPrefs);
342
+ // eslint-disable-next-line react-hooks/exhaustive-deps
343
+ }, [schedule]);
344
+
345
+ React.useEffect(() => {
346
+ if (!hasMounted.current) return;
347
+ onSave?.(schedule, currentPrefs);
348
+ // eslint-disable-next-line react-hooks/exhaustive-deps
349
+ }, [meetingDuration, schedulingBuffer, maxSlotsPerDay]);
350
+
351
+ React.useEffect(() => {
352
+ if (!hasMounted.current) return;
353
+ const blocked = timeOffEntries
354
+ .filter((e) => e.enabled)
355
+ .map(({ date, label, timeStart, timeEnd }) => ({
356
+ date,
357
+ label,
358
+ timeStart,
359
+ timeEnd,
360
+ }));
361
+ onBlockedDatesChange?.(blocked);
362
+ // eslint-disable-next-line react-hooks/exhaustive-deps
363
+ }, [timeOffEntries]);
364
+
365
+ // ---------------------------------------------------------------------------
366
+ // Weekly handlers
367
+ // ---------------------------------------------------------------------------
368
+
369
+ const toggleDay = (index: number) => {
370
+ setSchedule((prev) =>
371
+ prev.map((d, i) => (i === index ? { ...d, enabled: !d.enabled } : d)),
372
+ );
373
+ };
374
+
375
+ const updateTime = (
376
+ index: number,
377
+ field: "startTime" | "endTime",
378
+ value: string,
379
+ ) => {
380
+ setSchedule((prev) =>
381
+ prev.map((d, i) => (i === index ? { ...d, [field]: value } : d)),
382
+ );
383
+ };
384
+
385
+ // ---------------------------------------------------------------------------
386
+ // Time Off handlers
387
+ // ---------------------------------------------------------------------------
388
+
389
+ const toggleTimeOff = (date: string, enabled: boolean) => {
390
+ setTimeOffEntries((prev) =>
391
+ prev.map((e) => (e.date === date ? { ...e, enabled } : e)),
392
+ );
393
+ };
394
+
395
+ const removeCustomDate = (date: string) => {
396
+ setTimeOffEntries((prev) => prev.filter((e) => e.date !== date));
397
+ };
398
+
399
+ const handleAddMore = (entry: {
400
+ date: string;
401
+ label?: string;
402
+ timeStart?: string;
403
+ timeEnd?: string;
404
+ }) => {
405
+ if (timeOffEntries.some((e) => e.date === entry.date)) return;
406
+ setTimeOffEntries((prev) =>
407
+ [
408
+ ...prev,
409
+ {
410
+ date: entry.date,
411
+ label: entry.label,
412
+ enabled: true,
413
+ isHoliday: false,
414
+ timeStart: entry.timeStart,
415
+ timeEnd: entry.timeEnd,
416
+ },
417
+ ].sort((a, b) => a.date.localeCompare(b.date)),
418
+ );
419
+ };
420
+
421
+ return (
422
+ <div className="border border-border bg-card">
423
+ <Tabs defaultValue="weekly" className="gap-0">
424
+ {/* Tab strip */}
425
+ <div className="border-b border-border p-1">
426
+ <TabsList variant="default" className="w-full">
427
+ <TabsTrigger value="weekly">Weekly Availability</TabsTrigger>
428
+ <TabsTrigger value="booking">Booking Preferences</TabsTrigger>
429
+ <TabsTrigger value="time-off">Time Off</TabsTrigger>
430
+ </TabsList>
431
+ </div>
432
+
433
+ {/* ---------------------------------------------------------------- */}
434
+ {/* Tab 1: Weekly Availability */}
435
+ {/* ---------------------------------------------------------------- */}
436
+ <TabsContent value="weekly">
437
+ <div className="flex flex-col divide-y divide-border">
438
+ {schedule.map((day, index) => (
439
+ <div
440
+ key={day.day}
441
+ className="flex min-h-[68px] items-center gap-5 px-6 py-4"
442
+ >
443
+ <div className="flex w-28 items-center gap-3">
444
+ <Switch
445
+ id={`day-${day.day}`}
446
+ checked={day.enabled}
447
+ onCheckedChange={() => toggleDay(index)}
448
+ />
449
+ <Label
450
+ htmlFor={`day-${day.day}`}
451
+ className="cursor-pointer text-sm font-medium"
452
+ >
453
+ {day.day}
454
+ </Label>
455
+ </div>
456
+
457
+ {day.enabled ? (
458
+ <div className="flex flex-1 items-center gap-3">
459
+ <TimeSelect
460
+ value={day.startTime}
461
+ onChange={(v) => updateTime(index, "startTime", v)}
462
+ />
463
+ <span className="text-sm text-muted-foreground">to</span>
464
+ <TimeSelect
465
+ value={day.endTime}
466
+ onChange={(v) => updateTime(index, "endTime", v)}
467
+ />
468
+ </div>
469
+ ) : (
470
+ <p className="flex-1 text-sm text-muted-foreground">
471
+ Unavailable
472
+ </p>
473
+ )}
474
+ </div>
475
+ ))}
476
+ </div>
477
+ </TabsContent>
478
+
479
+ {/* ---------------------------------------------------------------- */}
480
+ {/* Tab 2: Booking Preferences */}
481
+ {/* ---------------------------------------------------------------- */}
482
+ <TabsContent value="booking">
483
+ <div className="flex flex-col divide-y divide-border">
484
+ <div className="flex min-h-[68px] items-center justify-between px-6 py-4">
485
+ <div className="flex flex-col gap-0.5">
486
+ <p className="text-sm font-medium">Meeting Duration</p>
487
+ <p className="text-xs text-muted-foreground">
488
+ Length of each appointment slot
489
+ </p>
490
+ </div>
491
+ <Select
492
+ value={meetingDuration}
493
+ onValueChange={(v) => setMeetingDuration(v as string)}
494
+ >
495
+ <SelectTrigger className="w-40">
496
+ <SelectValue />
497
+ </SelectTrigger>
498
+ <SelectContent>
499
+ <SelectItem value="15">15 minutes</SelectItem>
500
+ <SelectItem value="30">30 minutes</SelectItem>
501
+ <SelectItem value="45">45 minutes</SelectItem>
502
+ <SelectItem value="60">60 minutes</SelectItem>
503
+ <SelectItem value="90">90 minutes</SelectItem>
504
+ </SelectContent>
505
+ </Select>
506
+ </div>
507
+
508
+ <div className="flex min-h-[68px] items-center justify-between px-6 py-4">
509
+ <div className="flex flex-col gap-0.5">
510
+ <p className="text-sm font-medium">Scheduling Buffer</p>
511
+ <p className="text-xs text-muted-foreground">
512
+ Gap between consecutive bookings
513
+ </p>
514
+ </div>
515
+ <Select
516
+ value={schedulingBuffer}
517
+ onValueChange={(v) => setSchedulingBuffer(v as string)}
518
+ >
519
+ <SelectTrigger className="w-40">
520
+ <SelectValue />
521
+ </SelectTrigger>
522
+ <SelectContent>
523
+ <SelectItem value="0">No buffer</SelectItem>
524
+ <SelectItem value="5">5 minutes</SelectItem>
525
+ <SelectItem value="10">10 minutes</SelectItem>
526
+ <SelectItem value="15">15 minutes</SelectItem>
527
+ <SelectItem value="30">30 minutes</SelectItem>
528
+ </SelectContent>
529
+ </Select>
530
+ </div>
531
+
532
+ <div className="flex min-h-[68px] items-center justify-between px-6 py-4">
533
+ <div className="flex flex-col gap-0.5">
534
+ <p className="text-sm font-medium">Daily Slot Limit</p>
535
+ <p className="text-xs text-muted-foreground">
536
+ Maximum bookings accepted per day
537
+ </p>
538
+ </div>
539
+ <Select
540
+ value={maxSlotsPerDay}
541
+ onValueChange={(v) => setMaxSlotsPerDay(v as string)}
542
+ >
543
+ <SelectTrigger className="w-40">
544
+ <SelectValue />
545
+ </SelectTrigger>
546
+ <SelectContent>
547
+ <SelectItem value="2">2 per day</SelectItem>
548
+ <SelectItem value="4">4 per day</SelectItem>
549
+ <SelectItem value="6">6 per day</SelectItem>
550
+ <SelectItem value="8">8 per day</SelectItem>
551
+ <SelectItem value="10">10 per day</SelectItem>
552
+ <SelectItem value="unlimited">Unlimited</SelectItem>
553
+ </SelectContent>
554
+ </Select>
555
+ </div>
556
+ </div>
557
+ </TabsContent>
558
+
559
+ {/* ---------------------------------------------------------------- */}
560
+ {/* Tab 3: Time Off */}
561
+ {/* ---------------------------------------------------------------- */}
562
+ <TabsContent value="time-off">
563
+ {/* Description + Add more button */}
564
+ <div className="flex items-center justify-between px-6 py-4">
565
+ <p className="text-sm text-muted-foreground">
566
+ Toggle dates when you are unavailable. Clients cannot book on
567
+ switched-on dates.
568
+ </p>
569
+ <Button
570
+ variant="outline"
571
+ size="sm"
572
+ className="ml-4 shrink-0 gap-1.5"
573
+ onClick={() => setAddMoreOpen(true)}
574
+ >
575
+ <Plus className="h-3.5 w-3.5" />
576
+ Add more
577
+ </Button>
578
+ </div>
579
+
580
+ <Separator />
581
+
582
+ {/* Holiday + custom date rows */}
583
+ <div className="flex flex-col divide-y divide-border">
584
+ {timeOffEntries.map((entry) => {
585
+ const formattedDate = formatDateWithWeekday(entry.date);
586
+ const hasTimeRange = entry.timeStart && entry.timeEnd;
587
+ return (
588
+ <div
589
+ key={entry.date}
590
+ className="flex min-h-[68px] items-center gap-5 px-6 py-4"
591
+ >
592
+ <Switch
593
+ id={`toff-${entry.date}`}
594
+ checked={entry.enabled}
595
+ onCheckedChange={(v) => toggleTimeOff(entry.date, v)}
596
+ />
597
+ <div className="min-w-0 flex-1">
598
+ <Label
599
+ htmlFor={`toff-${entry.date}`}
600
+ className={cn(
601
+ "cursor-pointer text-sm font-medium",
602
+ !entry.enabled && "text-muted-foreground",
603
+ )}
604
+ >
605
+ {entry.label ?? formattedDate}
606
+ </Label>
607
+ <p className="text-xs text-muted-foreground">
608
+ {entry.label ? formattedDate : null}
609
+ {hasTimeRange && (
610
+ <>
611
+ {entry.label ? " · " : ""}
612
+ {timeLabel(entry.timeStart!)} –{" "}
613
+ {timeLabel(entry.timeEnd!)}
614
+ </>
615
+ )}
616
+ </p>
617
+ </div>
618
+ {/* Only custom (non-holiday) dates can be removed */}
619
+ {!entry.isHoliday && (
620
+ <Button
621
+ type="button"
622
+ variant="ghost"
623
+ size="icon"
624
+ className="h-7 w-7 text-muted-foreground hover:text-destructive"
625
+ onClick={() => removeCustomDate(entry.date)}
626
+ >
627
+ <X className="h-3.5 w-3.5" />
628
+ </Button>
629
+ )}
630
+ </div>
631
+ );
632
+ })}
633
+ </div>
634
+ </TabsContent>
635
+ </Tabs>
636
+
637
+ {/* Add More dialog */}
638
+ <AddTimeOffDialog
639
+ open={addMoreOpen}
640
+ onOpenChange={setAddMoreOpen}
641
+ onAdd={handleAddMore}
642
+ />
643
+ </div>
644
+ );
645
+ }