@wealthx/shadcn 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/.turbo/turbo-build.log +203 -150
  2. package/CHANGELOG.md +29 -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-SYOD63OZ.mjs → chunk-5FQIKDKP.mjs} +6 -6
  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-WOEHFRGB.mjs → chunk-BDYZCBRT.mjs} +4 -4
  20. package/dist/{chunk-WAZD7NFU.mjs → chunk-BKNFWEH2.mjs} +6 -6
  21. package/dist/{chunk-CLIN5525.mjs → chunk-C7CQJNMR.mjs} +1 -1
  22. package/dist/{chunk-D4ILTPOG.mjs → chunk-CFMQP5QS.mjs} +5 -4
  23. package/dist/{chunk-VPBN3WOO.mjs → chunk-DGHAXJBN.mjs} +9 -7
  24. package/dist/chunk-DOEO3CDL.mjs +27 -0
  25. package/dist/{chunk-KUDCQ4FI.mjs → chunk-DUJTAXMH.mjs} +9 -4
  26. package/dist/{chunk-GGM2UYGG.mjs → chunk-EBXQWIYG.mjs} +10 -4
  27. package/dist/{chunk-PMB3A7V3.mjs → chunk-EI5F6FMT.mjs} +1 -1
  28. package/dist/chunk-EWRB4PAD.mjs +468 -0
  29. package/dist/chunk-FAKPBKLT.mjs +253 -0
  30. package/dist/chunk-FNQXOAYJ.mjs +169 -0
  31. package/dist/{chunk-4CX4SBRO.mjs → chunk-GHC7LLUX.mjs} +14 -5
  32. package/dist/chunk-HBZLGDIN.mjs +507 -0
  33. package/dist/{chunk-SIZMLSRU.mjs → chunk-HISNT2MG.mjs} +8 -6
  34. package/dist/{chunk-PR6V5XKM.mjs → chunk-HVY6KCCF.mjs} +7 -4
  35. package/dist/chunk-I3RZS7V2.mjs +136 -0
  36. package/dist/chunk-IAE3F7DR.mjs +1962 -0
  37. package/dist/{chunk-ZRO5JO3H.mjs → chunk-IHMFS7NZ.mjs} +81 -84
  38. package/dist/{chunk-PCPLO5HT.mjs → chunk-IOJRDS6V.mjs} +96 -14
  39. package/dist/{chunk-LHYCMLVA.mjs → chunk-JKGDCQTZ.mjs} +11 -4
  40. package/dist/{chunk-H45TKD34.mjs → chunk-JMHR3YGZ.mjs} +1 -1
  41. package/dist/{chunk-4MN6UQHG.mjs → chunk-K5A5L6T2.mjs} +17 -39
  42. package/dist/{chunk-CSDO6VBW.mjs → chunk-LBMRIB3G.mjs} +10 -10
  43. package/dist/chunk-LV35NGVG.mjs +272 -0
  44. package/dist/{chunk-FZIXGLMV.mjs → chunk-M3FV7LOK.mjs} +5 -12
  45. package/dist/{chunk-FMAXJ2SI.mjs → chunk-MBON7YRJ.mjs} +1 -1
  46. package/dist/chunk-MIZQHHUO.mjs +441 -0
  47. package/dist/chunk-MN5NYQCL.mjs +29 -0
  48. package/dist/chunk-NL3ZO62D.mjs +31 -0
  49. package/dist/{chunk-Q76O3RIQ.mjs → chunk-NMOI6CQD.mjs} +1 -1
  50. package/dist/{chunk-P6AM5V7O.mjs → chunk-OODBHKG7.mjs} +1 -1
  51. package/dist/chunk-PBL4OQV2.mjs +283 -0
  52. package/dist/{chunk-3WMX6KWS.mjs → chunk-PU4YZQXV.mjs} +11 -12
  53. package/dist/chunk-QMY3AZJH.mjs +80 -0
  54. package/dist/{chunk-BL3DXM2X.mjs → chunk-QZ4RE6NA.mjs} +11 -4
  55. package/dist/{chunk-VACKZOMY.mjs → chunk-R3VSPKNP.mjs} +3 -3
  56. package/dist/{chunk-OPNQAVVH.mjs → chunk-RJI6GKVF.mjs} +8 -6
  57. package/dist/{chunk-WG6JGJXB.mjs → chunk-T4BJLT57.mjs} +1 -1
  58. package/dist/chunk-U4NDAF2P.mjs +207 -0
  59. package/dist/{chunk-DOH3EHX7.mjs → chunk-U5X52X37.mjs} +1 -1
  60. package/dist/chunk-UMTOX62O.mjs +415 -0
  61. package/dist/{chunk-7MMXNK3C.mjs → chunk-VLARHE5V.mjs} +8 -6
  62. package/dist/{chunk-2I5S2AMY.mjs → chunk-XREGSKX3.mjs} +2 -2
  63. package/dist/{chunk-JNQORUPP.mjs → chunk-YJG55G2H.mjs} +14 -11
  64. package/dist/chunk-ZC45IGZO.mjs +388 -0
  65. package/dist/components/ui/add-column-modal.js +42 -14
  66. package/dist/components/ui/add-column-modal.mjs +4 -4
  67. package/dist/components/ui/add-lead-modal.js +42 -11
  68. package/dist/components/ui/add-lead-modal.mjs +3 -3
  69. package/dist/components/ui/advisor-card.js +497 -0
  70. package/dist/components/ui/advisor-card.mjs +13 -0
  71. package/dist/components/ui/ai-assistant-drawer.js +11 -10
  72. package/dist/components/ui/ai-assistant-drawer.mjs +3 -3
  73. package/dist/components/ui/alert-dialog.js +2 -2
  74. package/dist/components/ui/alert-dialog.mjs +2 -2
  75. package/dist/components/ui/appointment-action-dialogs.js +1160 -0
  76. package/dist/components/ui/appointment-action-dialogs.mjs +23 -0
  77. package/dist/components/ui/appointment-availability-settings.js +1590 -0
  78. package/dist/components/ui/appointment-availability-settings.mjs +23 -0
  79. package/dist/components/ui/appointment-book-dialog.js +1744 -0
  80. package/dist/components/ui/appointment-book-dialog.mjs +27 -0
  81. package/dist/components/ui/appointment-calendar-view.js +833 -0
  82. package/dist/components/ui/appointment-calendar-view.mjs +14 -0
  83. package/dist/components/ui/appointment-detail-sheet.js +1517 -0
  84. package/dist/components/ui/appointment-detail-sheet.mjs +24 -0
  85. package/dist/components/ui/appointment-gmail-connect.js +467 -0
  86. package/dist/components/ui/appointment-gmail-connect.mjs +14 -0
  87. package/dist/components/ui/appointment-mini-card.js +345 -0
  88. package/dist/components/ui/appointment-mini-card.mjs +11 -0
  89. package/dist/components/ui/appointment-time-slot-picker.js +311 -0
  90. package/dist/components/ui/appointment-time-slot-picker.mjs +13 -0
  91. package/dist/components/ui/appointment-upcoming-card.js +1268 -0
  92. package/dist/components/ui/appointment-upcoming-card.mjs +21 -0
  93. package/dist/components/ui/backoffice-alert-history-chart.js +11 -5
  94. package/dist/components/ui/backoffice-alert-history-chart.mjs +5 -4
  95. package/dist/components/ui/backoffice-alerts-chart.js +786 -0
  96. package/dist/components/ui/backoffice-alerts-chart.mjs +19 -0
  97. package/dist/components/ui/backoffice-connections-chart.js +817 -0
  98. package/dist/components/ui/backoffice-connections-chart.mjs +19 -0
  99. package/dist/components/ui/backoffice-contact-history-chart.js +11 -5
  100. package/dist/components/ui/backoffice-contact-history-chart.mjs +5 -4
  101. package/dist/components/ui/badge.js +6 -6
  102. package/dist/components/ui/badge.mjs +1 -1
  103. package/dist/components/ui/borrowing-capacity-line-chart.js +30 -21
  104. package/dist/components/ui/borrowing-capacity-line-chart.mjs +5 -4
  105. package/dist/components/ui/button.js +2 -2
  106. package/dist/components/ui/button.mjs +1 -1
  107. package/dist/components/ui/calendar.js +2 -2
  108. package/dist/components/ui/calendar.mjs +2 -2
  109. package/dist/components/ui/card.js +1 -1
  110. package/dist/components/ui/card.mjs +1 -1
  111. package/dist/components/ui/cash-balance-line-chart.js +31 -23
  112. package/dist/components/ui/cash-balance-line-chart.mjs +5 -4
  113. package/dist/components/ui/cashflow-bar-chart.js +12 -5
  114. package/dist/components/ui/cashflow-bar-chart.mjs +5 -4
  115. package/dist/components/ui/chip.js +97 -18
  116. package/dist/components/ui/chip.mjs +3 -2
  117. package/dist/components/ui/color-picker.js +547 -0
  118. package/dist/components/ui/color-picker.mjs +24 -0
  119. package/dist/components/ui/data-table.js +182 -129
  120. package/dist/components/ui/data-table.mjs +3 -2
  121. package/dist/components/ui/date-picker.js +48 -27
  122. package/dist/components/ui/date-picker.mjs +4 -3
  123. package/dist/components/ui/dialog.js +37 -9
  124. package/dist/components/ui/dialog.mjs +2 -2
  125. package/dist/components/ui/expense-bar-chart.js +12 -5
  126. package/dist/components/ui/expense-bar-chart.mjs +5 -4
  127. package/dist/components/ui/field.mjs +2 -2
  128. package/dist/components/ui/financial-cards.js +322 -155
  129. package/dist/components/ui/financial-cards.mjs +5 -3
  130. package/dist/components/ui/financial-drawers.js +2 -2
  131. package/dist/components/ui/financial-drawers.mjs +3 -3
  132. package/dist/components/ui/financial-sections.js +14 -10
  133. package/dist/components/ui/financial-sections.mjs +6 -5
  134. package/dist/components/ui/form-primitives.js +4 -4
  135. package/dist/components/ui/form-primitives.mjs +3 -3
  136. package/dist/components/ui/income-bar-chart.js +12 -5
  137. package/dist/components/ui/income-bar-chart.mjs +5 -4
  138. package/dist/components/ui/input-group.js +2 -2
  139. package/dist/components/ui/input-group.mjs +2 -2
  140. package/dist/components/ui/kanban-column.js +52 -44
  141. package/dist/components/ui/kanban-column.mjs +7 -5
  142. package/dist/components/ui/opportunity-card.js +52 -44
  143. package/dist/components/ui/opportunity-card.mjs +6 -4
  144. package/dist/components/ui/opportunity-edit-modals.js +1371 -1267
  145. package/dist/components/ui/opportunity-edit-modals.mjs +10 -10
  146. package/dist/components/ui/opportunity-summary-tab.js +2748 -2161
  147. package/dist/components/ui/opportunity-summary-tab.mjs +16 -16
  148. package/dist/components/ui/page-header.js +92 -0
  149. package/dist/components/ui/page-header.mjs +8 -0
  150. package/dist/components/ui/page-top-bar.js +88 -0
  151. package/dist/components/ui/page-top-bar.mjs +8 -0
  152. package/dist/components/ui/pagination.js +303 -19
  153. package/dist/components/ui/pagination.mjs +11 -4
  154. package/dist/components/ui/pipeline-board.js +209 -195
  155. package/dist/components/ui/pipeline-board.mjs +10 -8
  156. package/dist/components/ui/pipeline-dialogs.js +118 -69
  157. package/dist/components/ui/pipeline-dialogs.mjs +8 -7
  158. package/dist/components/ui/pipeline-primitives.js +6 -6
  159. package/dist/components/ui/pipeline-primitives.mjs +2 -2
  160. package/dist/components/ui/property-cashflow-doughnut-chart.js +14 -12
  161. package/dist/components/ui/property-cashflow-doughnut-chart.mjs +5 -4
  162. package/dist/components/ui/property-debt-equity-doughnut-chart.js +14 -12
  163. package/dist/components/ui/property-debt-equity-doughnut-chart.mjs +5 -4
  164. package/dist/components/ui/property-mobile-estimate-line-chart.js +16 -14
  165. package/dist/components/ui/property-mobile-estimate-line-chart.mjs +5 -4
  166. package/dist/components/ui/sidebar-nav.js +679 -0
  167. package/dist/components/ui/sidebar-nav.mjs +14 -0
  168. package/dist/components/ui/stage-timeline.js +6 -6
  169. package/dist/components/ui/stage-timeline.mjs +3 -3
  170. package/dist/components/ui/stepper.js +283 -0
  171. package/dist/components/ui/stepper.mjs +18 -0
  172. package/dist/components/ui/toggle-group.js +4 -4
  173. package/dist/components/ui/toggle-group.mjs +2 -2
  174. package/dist/components/ui/toggle.js +4 -4
  175. package/dist/components/ui/toggle.mjs +1 -1
  176. package/dist/components/ui/transactions-expense-categories-doughnut-chart.js +18 -16
  177. package/dist/components/ui/transactions-expense-categories-doughnut-chart.mjs +5 -4
  178. package/dist/components/ui/transactions-income-expense-bar-chart.js +28 -12
  179. package/dist/components/ui/transactions-income-expense-bar-chart.mjs +5 -4
  180. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.js +18 -16
  181. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.mjs +5 -4
  182. package/dist/index.js +12927 -8522
  183. package/dist/index.mjs +288 -190
  184. package/dist/lib/typography.js +10 -10
  185. package/dist/lib/typography.mjs +1 -1
  186. package/dist/styles.css +1 -1
  187. package/package.json +86 -1
  188. package/src/components/index.tsx +146 -0
  189. package/src/components/ui/add-column-modal.tsx +7 -7
  190. package/src/components/ui/add-lead-modal.tsx +6 -3
  191. package/src/components/ui/advisor-card.tsx +227 -0
  192. package/src/components/ui/ai-assistant-drawer.tsx +4 -3
  193. package/src/components/ui/appointment-action-dialogs.tsx +297 -0
  194. package/src/components/ui/appointment-availability-settings.tsx +645 -0
  195. package/src/components/ui/appointment-book-dialog.tsx +618 -0
  196. package/src/components/ui/appointment-calendar-view.tsx +510 -0
  197. package/src/components/ui/appointment-detail-sheet.tsx +415 -0
  198. package/src/components/ui/appointment-gmail-connect.tsx +188 -0
  199. package/src/components/ui/appointment-mini-card.tsx +104 -0
  200. package/src/components/ui/appointment-time-slot-picker.tsx +123 -0
  201. package/src/components/ui/appointment-upcoming-card.tsx +635 -0
  202. package/src/components/ui/backoffice-alert-history-chart.tsx +10 -2
  203. package/src/components/ui/backoffice-alerts-chart.tsx +312 -0
  204. package/src/components/ui/backoffice-connections-chart.tsx +339 -0
  205. package/src/components/ui/backoffice-contact-history-chart.tsx +10 -2
  206. package/src/components/ui/badge.tsx +12 -6
  207. package/src/components/ui/borrowing-capacity-line-chart.tsx +4 -11
  208. package/src/components/ui/button.tsx +2 -2
  209. package/src/components/ui/card.tsx +1 -1
  210. package/src/components/ui/cash-balance-line-chart.tsx +4 -23
  211. package/src/components/ui/cashflow-bar-chart.tsx +9 -2
  212. package/src/components/ui/chart-shared.tsx +4 -11
  213. package/src/components/ui/chip.tsx +23 -19
  214. package/src/components/ui/color-picker.tsx +309 -0
  215. package/src/components/ui/data-table.tsx +117 -83
  216. package/src/components/ui/date-picker.tsx +42 -37
  217. package/src/components/ui/dialog.tsx +72 -6
  218. package/src/components/ui/expense-bar-chart.tsx +11 -2
  219. package/src/components/ui/financial-cards.tsx +99 -10
  220. package/src/components/ui/income-bar-chart.tsx +11 -2
  221. package/src/components/ui/opportunity-card.tsx +10 -39
  222. package/src/components/ui/opportunity-edit-modals.tsx +98 -36
  223. package/src/components/ui/opportunity-summary-tab.tsx +548 -232
  224. package/src/components/ui/page-header.tsx +57 -0
  225. package/src/components/ui/page-top-bar.tsx +48 -0
  226. package/src/components/ui/pagination.tsx +171 -22
  227. package/src/components/ui/pipeline-board.tsx +12 -5
  228. package/src/components/ui/property-cashflow-doughnut-chart.tsx +3 -1
  229. package/src/components/ui/property-debt-equity-doughnut-chart.tsx +3 -1
  230. package/src/components/ui/property-mobile-estimate-line-chart.tsx +3 -1
  231. package/src/components/ui/sidebar-nav.tsx +516 -0
  232. package/src/components/ui/stepper.tsx +347 -0
  233. package/src/components/ui/toggle.tsx +4 -4
  234. package/src/components/ui/transactions-expense-categories-doughnut-chart.tsx +3 -1
  235. package/src/components/ui/transactions-income-expense-bar-chart.tsx +12 -9
  236. package/src/components/ui/transactions-liabilities-breakdown-doughnut-chart.tsx +3 -1
  237. package/src/lib/format-currency.ts +44 -0
  238. package/src/lib/format-date.ts +50 -0
  239. package/src/lib/opportunity-constants.ts +12 -0
  240. package/src/lib/typography.ts +11 -11
  241. package/src/styles/globals.css +36 -34
  242. package/src/styles/styles-css.ts +1 -1
  243. package/tsup.config.ts +17 -0
  244. package/dist/chunk-PG6K5XEC.mjs +0 -475
  245. package/dist/chunk-WA6O6EUR.mjs +0 -1885
  246. package/dist/chunk-WNGWBVLV.mjs +0 -148
  247. package/dist/{chunk-LLVQKSU3.mjs → chunk-GD4BJDJR.mjs} +3 -3
@@ -0,0 +1,510 @@
1
+ import type { ReactNode } from "react";
2
+ import { Avatar, AvatarFallback } from "./avatar";
3
+ import { Badge } from "./badge";
4
+ import { Button } from "./button";
5
+ import { Separator } from "./separator";
6
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
7
+ import {
8
+ Check,
9
+ ChevronLeft,
10
+ ChevronRight,
11
+ RefreshCw,
12
+ X,
13
+ CalendarClock,
14
+ } from "lucide-react";
15
+ import type { AppointmentStatus } from "./appointment-time-slot-picker";
16
+
17
+ // Re-export so consumers who import AppointmentStatus from this module still work
18
+ export type { AppointmentStatus } from "./appointment-time-slot-picker";
19
+
20
+ export interface AppointmentCalendarItem {
21
+ id: string;
22
+ status: AppointmentStatus;
23
+ clientName: string;
24
+ clientAvatarInitials: string;
25
+ /** ISO date string — "2026-04-22" */
26
+ date: string;
27
+ /** Display time — "10:00 AM" */
28
+ timeStart: string;
29
+ /** Display time — "10:30 AM" */
30
+ timeEnd: string;
31
+ }
32
+
33
+ export interface AppointmentWeekDay {
34
+ /** Short weekday label — "Mon" */
35
+ label: string;
36
+ /** ISO date string — "2026-04-20" */
37
+ date: string;
38
+ /** Short date number — "20" */
39
+ short: string;
40
+ }
41
+
42
+ export interface AppointmentCalendarViewProps {
43
+ appointments: AppointmentCalendarItem[];
44
+ /** ISO date string for the "today" highlight — "2026-04-22" */
45
+ today: string;
46
+ /** Label shown in the toolbar — "April 2026" */
47
+ periodLabel: string;
48
+ /** Days shown in week view */
49
+ weekDays: AppointmentWeekDay[];
50
+ /** Hours displayed in day/week view — defaults to [9..17] */
51
+ hours?: number[];
52
+ /** ISO date used for Day view */
53
+ dayViewDate?: string;
54
+ defaultView?: "day" | "week" | "month";
55
+ onPrev?: () => void;
56
+ onNext?: () => void;
57
+ onToday?: () => void;
58
+ onSelectAppointment?: (apt: AppointmentCalendarItem) => void;
59
+ /** Slot rendered in the calendar toolbar right side (e.g. pending request button + new appointment button) */
60
+ toolbarActions?: ReactNode;
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Status styling
65
+ // ---------------------------------------------------------------------------
66
+
67
+ const STATUS_COLORS: Record<AppointmentStatus, string> = {
68
+ pending: "border-l-warning bg-warning/5",
69
+ confirmed: "border-l-success bg-success/5",
70
+ cancelled: "border-l-destructive bg-destructive/5",
71
+ rescheduled: "border-l-info bg-info/5",
72
+ };
73
+
74
+ const STATUS_CONFIG: Record<
75
+ AppointmentStatus,
76
+ {
77
+ variant: "warning" | "success" | "destructive" | "info";
78
+ label: string;
79
+ icon: ReactNode;
80
+ }
81
+ > = {
82
+ pending: {
83
+ variant: "warning",
84
+ label: "Pending",
85
+ icon: <CalendarClock size={12} />,
86
+ },
87
+ confirmed: {
88
+ variant: "success",
89
+ label: "Confirmed",
90
+ icon: <Check size={12} />,
91
+ },
92
+ cancelled: {
93
+ variant: "destructive",
94
+ label: "Cancelled",
95
+ icon: <X size={12} />,
96
+ },
97
+ rescheduled: {
98
+ variant: "info",
99
+ label: "Rescheduled",
100
+ icon: <RefreshCw size={12} />,
101
+ },
102
+ };
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Helpers
106
+ // ---------------------------------------------------------------------------
107
+
108
+ function formatHour(h: number) {
109
+ if (h === 12) return "12 PM";
110
+ return h < 12 ? `${h} AM` : `${h - 12} PM`;
111
+ }
112
+
113
+ function getHourFromTime(time: string): number {
114
+ const match = time.match(/^(\d+):/);
115
+ if (!match) return 0;
116
+ const h = parseInt(match[1], 10);
117
+ return time.includes("PM") && h !== 12 ? h + 12 : h;
118
+ }
119
+
120
+ const DEFAULT_HOURS = [9, 10, 11, 12, 13, 14, 15, 16, 17];
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Day view
124
+ // ---------------------------------------------------------------------------
125
+
126
+ function DayView({
127
+ appointments,
128
+ date,
129
+ today,
130
+ hours,
131
+ onSelectAppointment,
132
+ }: {
133
+ appointments: AppointmentCalendarItem[];
134
+ date: string;
135
+ today: string;
136
+ hours: number[];
137
+ onSelectAppointment?: (apt: AppointmentCalendarItem) => void;
138
+ }) {
139
+ const d = new Date(date + "T00:00:00");
140
+ const dayLabel = d
141
+ .toLocaleDateString("en-AU", { weekday: "short" })
142
+ .toUpperCase();
143
+ const dayShort = d.getDate();
144
+ const isToday = date === today;
145
+ const dayApts = appointments.filter((a) => a.date === date);
146
+
147
+ return (
148
+ <div className="flex flex-col">
149
+ {/* Column header */}
150
+ <div className="grid grid-cols-[48px_1fr] border-b border-border">
151
+ <div className="border-r border-border" />
152
+ <div
153
+ className={`flex flex-col items-center gap-1 py-3 ${isToday ? "bg-primary/5" : ""}`}
154
+ >
155
+ <span
156
+ className={`text-xs font-medium tracking-wide ${isToday ? "font-semibold text-primary" : "text-muted-foreground"}`}
157
+ >
158
+ {dayLabel}
159
+ </span>
160
+ <span
161
+ className={`flex h-10 w-10 items-center justify-center text-xl font-semibold ${isToday ? "bg-primary text-primary-foreground" : ""}`}
162
+ >
163
+ {dayShort}
164
+ </span>
165
+ </div>
166
+ </div>
167
+
168
+ {/* Hourly grid */}
169
+ {hours.map((hour) => {
170
+ const aptsAtHour = dayApts.filter(
171
+ (a) => getHourFromTime(a.timeStart) === hour,
172
+ );
173
+ return (
174
+ <div
175
+ key={hour}
176
+ className="grid min-h-[60px] grid-cols-[48px_1fr] border-b border-border/40 last:border-b-0"
177
+ >
178
+ <div
179
+ className={`flex items-start justify-end border-r border-border pr-2 pt-1 ${isToday ? "bg-primary/5" : ""}`}
180
+ >
181
+ <span className="text-[10px] text-muted-foreground">
182
+ {formatHour(hour)}
183
+ </span>
184
+ </div>
185
+ <div
186
+ className={`flex flex-col gap-1.5 p-1.5 ${isToday ? "bg-primary/5" : ""}`}
187
+ >
188
+ {aptsAtHour.map((apt) => (
189
+ <Button
190
+ key={apt.id}
191
+ type="button"
192
+ variant="ghost"
193
+ onClick={() => onSelectAppointment?.(apt)}
194
+ className={`h-auto w-full justify-start gap-3 border-l-2 px-3 py-2 text-left hover:opacity-80 ${STATUS_COLORS[apt.status]}`}
195
+ >
196
+ <Avatar className="h-7 w-7 shrink-0">
197
+ <AvatarFallback className="text-xs">
198
+ {apt.clientAvatarInitials}
199
+ </AvatarFallback>
200
+ </Avatar>
201
+ <div className="flex min-w-0 flex-1 flex-col">
202
+ <span className="text-sm font-semibold">
203
+ {apt.clientName}
204
+ </span>
205
+ <span className="text-xs text-muted-foreground">
206
+ {apt.timeStart} – {apt.timeEnd}
207
+ </span>
208
+ </div>
209
+ <Badge variant={STATUS_CONFIG[apt.status].variant}>
210
+ {STATUS_CONFIG[apt.status].label}
211
+ </Badge>
212
+ </Button>
213
+ ))}
214
+ </div>
215
+ </div>
216
+ );
217
+ })}
218
+ </div>
219
+ );
220
+ }
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Week view
224
+ // ---------------------------------------------------------------------------
225
+
226
+ function WeekView({
227
+ appointments,
228
+ weekDays,
229
+ today,
230
+ hours,
231
+ onSelectAppointment,
232
+ }: {
233
+ appointments: AppointmentCalendarItem[];
234
+ weekDays: AppointmentWeekDay[];
235
+ today: string;
236
+ hours: number[];
237
+ onSelectAppointment?: (apt: AppointmentCalendarItem) => void;
238
+ }) {
239
+ return (
240
+ <div className="flex flex-col overflow-x-auto">
241
+ {/* Day headers */}
242
+ <div
243
+ className="grid border-b border-border"
244
+ style={{ gridTemplateColumns: `48px repeat(${weekDays.length}, 1fr)` }}
245
+ >
246
+ <div className="border-r border-border" />
247
+ {weekDays.map((day) => (
248
+ <div
249
+ key={day.date}
250
+ className={`flex flex-col items-center border-r border-border py-2 last:border-r-0 ${day.date === today ? "bg-primary/5" : ""}`}
251
+ >
252
+ <span
253
+ className={`text-xs font-medium ${day.date === today ? "font-semibold text-primary" : "text-muted-foreground"}`}
254
+ >
255
+ {day.label}
256
+ </span>
257
+ <span
258
+ className={`text-sm font-semibold ${day.date === today ? "text-primary" : ""}`}
259
+ >
260
+ {day.short}
261
+ </span>
262
+ </div>
263
+ ))}
264
+ </div>
265
+
266
+ {/* Time rows */}
267
+ {hours.map((hour) => (
268
+ <div
269
+ key={hour}
270
+ className="grid border-b border-border/40 last:border-b-0"
271
+ style={{
272
+ gridTemplateColumns: `48px repeat(${weekDays.length}, 1fr)`,
273
+ }}
274
+ >
275
+ <div className="flex items-start justify-end border-r border-border pr-2 pt-1">
276
+ <span className="text-[10px] text-muted-foreground">
277
+ {formatHour(hour)}
278
+ </span>
279
+ </div>
280
+ {weekDays.map((day) => {
281
+ const aptsAtCell = appointments.filter(
282
+ (a) =>
283
+ a.date === day.date && getHourFromTime(a.timeStart) === hour,
284
+ );
285
+ return (
286
+ <div
287
+ key={day.date}
288
+ className={`min-h-[44px] overflow-hidden border-r border-border/40 p-0.5 last:border-r-0 ${day.date === today ? "bg-primary/5" : ""}`}
289
+ >
290
+ {aptsAtCell.map((apt) => (
291
+ <Button
292
+ key={apt.id}
293
+ type="button"
294
+ variant="ghost"
295
+ className={`h-auto w-full justify-start truncate border-l-2 px-1 py-1 text-left text-xs font-medium hover:opacity-80 ${STATUS_COLORS[apt.status]}`}
296
+ onClick={() => onSelectAppointment?.(apt)}
297
+ >
298
+ {apt.timeStart} {apt.clientName}
299
+ </Button>
300
+ ))}
301
+ </div>
302
+ );
303
+ })}
304
+ </div>
305
+ ))}
306
+ </div>
307
+ );
308
+ }
309
+
310
+ // ---------------------------------------------------------------------------
311
+ // Month view
312
+ // ---------------------------------------------------------------------------
313
+
314
+ const MONTH_DAY_HEADERS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
315
+
316
+ function generateMonthGrid(year: number, month: number): (Date | null)[][] {
317
+ const firstDay = new Date(year, month, 1);
318
+ const lastDay = new Date(year, month + 1, 0);
319
+ const startDow = firstDay.getDay();
320
+ const grid: (Date | null)[][] = [];
321
+ let week: (Date | null)[] = Array.from({ length: startDow }, () => null);
322
+
323
+ for (let day = 1; day <= lastDay.getDate(); day++) {
324
+ week.push(new Date(year, month, day));
325
+ if (week.length === 7) {
326
+ grid.push(week);
327
+ week = [];
328
+ }
329
+ }
330
+ if (week.length > 0) {
331
+ while (week.length < 7) week.push(null);
332
+ grid.push(week);
333
+ }
334
+ return grid;
335
+ }
336
+
337
+ function MonthView({
338
+ appointments,
339
+ today,
340
+ onSelectAppointment,
341
+ }: {
342
+ appointments: AppointmentCalendarItem[];
343
+ today: string;
344
+ onSelectAppointment?: (apt: AppointmentCalendarItem) => void;
345
+ }) {
346
+ // Derive year/month from today prop
347
+ const todayDate = new Date(today + "T00:00:00");
348
+ const grid = generateMonthGrid(todayDate.getFullYear(), todayDate.getMonth());
349
+ const toIso = (d: Date) => d.toISOString().slice(0, 10);
350
+
351
+ return (
352
+ <div className="flex flex-col">
353
+ {/* Day-of-week header row */}
354
+ <div className="grid grid-cols-7 border-b border-border">
355
+ {MONTH_DAY_HEADERS.map((d) => (
356
+ <div
357
+ key={d}
358
+ className="flex items-center justify-center border-r border-border/40 py-2 last:border-r-0"
359
+ >
360
+ <span className="text-xs font-medium text-muted-foreground">
361
+ {d}
362
+ </span>
363
+ </div>
364
+ ))}
365
+ </div>
366
+
367
+ {/* Week rows */}
368
+ {grid.map((week, wi) => (
369
+ <div
370
+ key={wi}
371
+ className="grid grid-cols-7 border-b border-border last:border-b-0"
372
+ >
373
+ {week.map((day, di) => {
374
+ const iso = day ? toIso(day) : null;
375
+ const dayApts = iso
376
+ ? appointments.filter((a) => a.date === iso)
377
+ : [];
378
+ const isToday = iso === today;
379
+
380
+ return (
381
+ <div
382
+ key={di}
383
+ className={`min-h-[80px] border-r border-border/40 p-1 last:border-r-0 ${!day ? "bg-muted/20" : ""} ${isToday ? "bg-primary/5" : ""}`}
384
+ >
385
+ {day && (
386
+ <>
387
+ <div
388
+ className={`mb-1 flex h-6 w-6 items-center justify-center text-xs font-semibold ${isToday ? "bg-primary text-primary-foreground" : "text-foreground"}`}
389
+ >
390
+ {day.getDate()}
391
+ </div>
392
+ <div className="flex flex-col gap-0.5">
393
+ {dayApts.slice(0, 2).map((apt) => (
394
+ <Button
395
+ key={apt.id}
396
+ type="button"
397
+ variant="ghost"
398
+ onClick={() => onSelectAppointment?.(apt)}
399
+ className={`h-auto w-full justify-start truncate border-l-2 px-1 py-0.5 text-left text-[10px] font-medium hover:opacity-80 ${STATUS_COLORS[apt.status]}`}
400
+ >
401
+ {apt.timeStart} {apt.clientName}
402
+ </Button>
403
+ ))}
404
+ {dayApts.length > 2 && (
405
+ <p className="px-1 text-[10px] text-muted-foreground">
406
+ +{dayApts.length - 2} more
407
+ </p>
408
+ )}
409
+ </div>
410
+ </>
411
+ )}
412
+ </div>
413
+ );
414
+ })}
415
+ </div>
416
+ ))}
417
+ </div>
418
+ );
419
+ }
420
+
421
+ // ---------------------------------------------------------------------------
422
+ // Composite calendar view
423
+ // ---------------------------------------------------------------------------
424
+
425
+ export function AppointmentCalendarView({
426
+ appointments,
427
+ today,
428
+ periodLabel,
429
+ weekDays,
430
+ hours = DEFAULT_HOURS,
431
+ dayViewDate,
432
+ defaultView = "week",
433
+ onPrev,
434
+ onNext,
435
+ onToday,
436
+ onSelectAppointment,
437
+ toolbarActions,
438
+ }: AppointmentCalendarViewProps) {
439
+ const dayDate = dayViewDate ?? today;
440
+
441
+ return (
442
+ <div className="flex flex-col border border-border bg-card">
443
+ {/* Toolbar */}
444
+ <div className="flex items-center justify-between gap-4 px-4 py-3">
445
+ <div className="flex items-center gap-2">
446
+ <Button
447
+ variant="outline"
448
+ size="sm"
449
+ className="gap-1"
450
+ onClick={onPrev}
451
+ >
452
+ <ChevronLeft className="h-3.5 w-3.5" />
453
+ </Button>
454
+ <p className="text-sm font-medium">{periodLabel}</p>
455
+ <Button
456
+ variant="outline"
457
+ size="sm"
458
+ className="gap-1"
459
+ onClick={onNext}
460
+ >
461
+ <ChevronRight className="h-3.5 w-3.5" />
462
+ </Button>
463
+ <Button variant="outline" size="sm" onClick={onToday}>
464
+ Today
465
+ </Button>
466
+ </div>
467
+ {toolbarActions && (
468
+ <div className="flex items-center gap-2">{toolbarActions}</div>
469
+ )}
470
+ </div>
471
+
472
+ <Separator />
473
+
474
+ <Tabs defaultValue={defaultView} className="flex flex-col">
475
+ <div className="px-4 pt-3">
476
+ <TabsList>
477
+ <TabsTrigger value="day">Day</TabsTrigger>
478
+ <TabsTrigger value="week">Week</TabsTrigger>
479
+ <TabsTrigger value="month">Month</TabsTrigger>
480
+ </TabsList>
481
+ </div>
482
+ <TabsContent value="day" className="mt-0">
483
+ <DayView
484
+ appointments={appointments}
485
+ date={dayDate}
486
+ today={today}
487
+ hours={hours}
488
+ onSelectAppointment={onSelectAppointment}
489
+ />
490
+ </TabsContent>
491
+ <TabsContent value="week" className="mt-0">
492
+ <WeekView
493
+ appointments={appointments}
494
+ weekDays={weekDays}
495
+ today={today}
496
+ hours={hours}
497
+ onSelectAppointment={onSelectAppointment}
498
+ />
499
+ </TabsContent>
500
+ <TabsContent value="month" className="mt-0">
501
+ <MonthView
502
+ appointments={appointments}
503
+ today={today}
504
+ onSelectAppointment={onSelectAppointment}
505
+ />
506
+ </TabsContent>
507
+ </Tabs>
508
+ </div>
509
+ );
510
+ }