@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.
- package/.turbo/turbo-build.log +203 -150
- package/CHANGELOG.md +29 -0
- package/dist/{chunk-4Y6R4WEC.mjs → chunk-2A5RRQGG.mjs} +9 -22
- package/dist/{chunk-TS2ZX2VS.mjs → chunk-2UM72RJ7.mjs} +11 -15
- package/dist/{chunk-A56YQQHG.mjs → chunk-3NCUZIFP.mjs} +2 -2
- package/dist/chunk-3OYFOX3X.mjs +79 -0
- package/dist/{chunk-RP3SQYA3.mjs → chunk-3TTACBDP.mjs} +9 -4
- package/dist/chunk-4GAWMKMI.mjs +710 -0
- package/dist/{chunk-SYOD63OZ.mjs → chunk-5FQIKDKP.mjs} +6 -6
- package/dist/{chunk-K3JYD4IU.mjs → chunk-5IS7G74I.mjs} +11 -4
- package/dist/chunk-6AW4KJHE.mjs +235 -0
- package/dist/chunk-6CR5N2JW.mjs +302 -0
- package/dist/{chunk-XIRTEFKH.mjs → chunk-6DZEXFNB.mjs} +36 -8
- package/dist/chunk-6O6KD7CE.mjs +271 -0
- package/dist/chunk-7PV3IWCN.mjs +33 -0
- package/dist/{chunk-SPJ5KXW7.mjs → chunk-7S5AESZO.mjs} +5 -5
- package/dist/{chunk-RYCLWMZ7.mjs → chunk-ABFDMHOR.mjs} +9 -7
- package/dist/{chunk-SWGT756Z.mjs → chunk-AMQZRHEZ.mjs} +10 -4
- package/dist/{chunk-WOEHFRGB.mjs → chunk-BDYZCBRT.mjs} +4 -4
- package/dist/{chunk-WAZD7NFU.mjs → chunk-BKNFWEH2.mjs} +6 -6
- package/dist/{chunk-CLIN5525.mjs → chunk-C7CQJNMR.mjs} +1 -1
- package/dist/{chunk-D4ILTPOG.mjs → chunk-CFMQP5QS.mjs} +5 -4
- package/dist/{chunk-VPBN3WOO.mjs → chunk-DGHAXJBN.mjs} +9 -7
- package/dist/chunk-DOEO3CDL.mjs +27 -0
- package/dist/{chunk-KUDCQ4FI.mjs → chunk-DUJTAXMH.mjs} +9 -4
- package/dist/{chunk-GGM2UYGG.mjs → chunk-EBXQWIYG.mjs} +10 -4
- package/dist/{chunk-PMB3A7V3.mjs → chunk-EI5F6FMT.mjs} +1 -1
- package/dist/chunk-EWRB4PAD.mjs +468 -0
- package/dist/chunk-FAKPBKLT.mjs +253 -0
- package/dist/chunk-FNQXOAYJ.mjs +169 -0
- package/dist/{chunk-4CX4SBRO.mjs → chunk-GHC7LLUX.mjs} +14 -5
- package/dist/chunk-HBZLGDIN.mjs +507 -0
- package/dist/{chunk-SIZMLSRU.mjs → chunk-HISNT2MG.mjs} +8 -6
- package/dist/{chunk-PR6V5XKM.mjs → chunk-HVY6KCCF.mjs} +7 -4
- package/dist/chunk-I3RZS7V2.mjs +136 -0
- package/dist/chunk-IAE3F7DR.mjs +1962 -0
- package/dist/{chunk-ZRO5JO3H.mjs → chunk-IHMFS7NZ.mjs} +81 -84
- package/dist/{chunk-PCPLO5HT.mjs → chunk-IOJRDS6V.mjs} +96 -14
- package/dist/{chunk-LHYCMLVA.mjs → chunk-JKGDCQTZ.mjs} +11 -4
- package/dist/{chunk-H45TKD34.mjs → chunk-JMHR3YGZ.mjs} +1 -1
- package/dist/{chunk-4MN6UQHG.mjs → chunk-K5A5L6T2.mjs} +17 -39
- package/dist/{chunk-CSDO6VBW.mjs → chunk-LBMRIB3G.mjs} +10 -10
- package/dist/chunk-LV35NGVG.mjs +272 -0
- package/dist/{chunk-FZIXGLMV.mjs → chunk-M3FV7LOK.mjs} +5 -12
- package/dist/{chunk-FMAXJ2SI.mjs → chunk-MBON7YRJ.mjs} +1 -1
- package/dist/chunk-MIZQHHUO.mjs +441 -0
- package/dist/chunk-MN5NYQCL.mjs +29 -0
- package/dist/chunk-NL3ZO62D.mjs +31 -0
- package/dist/{chunk-Q76O3RIQ.mjs → chunk-NMOI6CQD.mjs} +1 -1
- package/dist/{chunk-P6AM5V7O.mjs → chunk-OODBHKG7.mjs} +1 -1
- package/dist/chunk-PBL4OQV2.mjs +283 -0
- package/dist/{chunk-3WMX6KWS.mjs → chunk-PU4YZQXV.mjs} +11 -12
- package/dist/chunk-QMY3AZJH.mjs +80 -0
- package/dist/{chunk-BL3DXM2X.mjs → chunk-QZ4RE6NA.mjs} +11 -4
- package/dist/{chunk-VACKZOMY.mjs → chunk-R3VSPKNP.mjs} +3 -3
- package/dist/{chunk-OPNQAVVH.mjs → chunk-RJI6GKVF.mjs} +8 -6
- package/dist/{chunk-WG6JGJXB.mjs → chunk-T4BJLT57.mjs} +1 -1
- package/dist/chunk-U4NDAF2P.mjs +207 -0
- package/dist/{chunk-DOH3EHX7.mjs → chunk-U5X52X37.mjs} +1 -1
- package/dist/chunk-UMTOX62O.mjs +415 -0
- package/dist/{chunk-7MMXNK3C.mjs → chunk-VLARHE5V.mjs} +8 -6
- package/dist/{chunk-2I5S2AMY.mjs → chunk-XREGSKX3.mjs} +2 -2
- package/dist/{chunk-JNQORUPP.mjs → chunk-YJG55G2H.mjs} +14 -11
- package/dist/chunk-ZC45IGZO.mjs +388 -0
- package/dist/components/ui/add-column-modal.js +42 -14
- package/dist/components/ui/add-column-modal.mjs +4 -4
- package/dist/components/ui/add-lead-modal.js +42 -11
- package/dist/components/ui/add-lead-modal.mjs +3 -3
- package/dist/components/ui/advisor-card.js +497 -0
- package/dist/components/ui/advisor-card.mjs +13 -0
- package/dist/components/ui/ai-assistant-drawer.js +11 -10
- package/dist/components/ui/ai-assistant-drawer.mjs +3 -3
- package/dist/components/ui/alert-dialog.js +2 -2
- package/dist/components/ui/alert-dialog.mjs +2 -2
- package/dist/components/ui/appointment-action-dialogs.js +1160 -0
- package/dist/components/ui/appointment-action-dialogs.mjs +23 -0
- package/dist/components/ui/appointment-availability-settings.js +1590 -0
- package/dist/components/ui/appointment-availability-settings.mjs +23 -0
- package/dist/components/ui/appointment-book-dialog.js +1744 -0
- package/dist/components/ui/appointment-book-dialog.mjs +27 -0
- package/dist/components/ui/appointment-calendar-view.js +833 -0
- package/dist/components/ui/appointment-calendar-view.mjs +14 -0
- package/dist/components/ui/appointment-detail-sheet.js +1517 -0
- package/dist/components/ui/appointment-detail-sheet.mjs +24 -0
- package/dist/components/ui/appointment-gmail-connect.js +467 -0
- package/dist/components/ui/appointment-gmail-connect.mjs +14 -0
- package/dist/components/ui/appointment-mini-card.js +345 -0
- package/dist/components/ui/appointment-mini-card.mjs +11 -0
- package/dist/components/ui/appointment-time-slot-picker.js +311 -0
- package/dist/components/ui/appointment-time-slot-picker.mjs +13 -0
- package/dist/components/ui/appointment-upcoming-card.js +1268 -0
- package/dist/components/ui/appointment-upcoming-card.mjs +21 -0
- package/dist/components/ui/backoffice-alert-history-chart.js +11 -5
- package/dist/components/ui/backoffice-alert-history-chart.mjs +5 -4
- package/dist/components/ui/backoffice-alerts-chart.js +786 -0
- package/dist/components/ui/backoffice-alerts-chart.mjs +19 -0
- package/dist/components/ui/backoffice-connections-chart.js +817 -0
- package/dist/components/ui/backoffice-connections-chart.mjs +19 -0
- package/dist/components/ui/backoffice-contact-history-chart.js +11 -5
- package/dist/components/ui/backoffice-contact-history-chart.mjs +5 -4
- package/dist/components/ui/badge.js +6 -6
- package/dist/components/ui/badge.mjs +1 -1
- package/dist/components/ui/borrowing-capacity-line-chart.js +30 -21
- package/dist/components/ui/borrowing-capacity-line-chart.mjs +5 -4
- package/dist/components/ui/button.js +2 -2
- package/dist/components/ui/button.mjs +1 -1
- package/dist/components/ui/calendar.js +2 -2
- package/dist/components/ui/calendar.mjs +2 -2
- package/dist/components/ui/card.js +1 -1
- package/dist/components/ui/card.mjs +1 -1
- package/dist/components/ui/cash-balance-line-chart.js +31 -23
- package/dist/components/ui/cash-balance-line-chart.mjs +5 -4
- package/dist/components/ui/cashflow-bar-chart.js +12 -5
- package/dist/components/ui/cashflow-bar-chart.mjs +5 -4
- package/dist/components/ui/chip.js +97 -18
- package/dist/components/ui/chip.mjs +3 -2
- package/dist/components/ui/color-picker.js +547 -0
- package/dist/components/ui/color-picker.mjs +24 -0
- package/dist/components/ui/data-table.js +182 -129
- package/dist/components/ui/data-table.mjs +3 -2
- package/dist/components/ui/date-picker.js +48 -27
- package/dist/components/ui/date-picker.mjs +4 -3
- package/dist/components/ui/dialog.js +37 -9
- package/dist/components/ui/dialog.mjs +2 -2
- package/dist/components/ui/expense-bar-chart.js +12 -5
- package/dist/components/ui/expense-bar-chart.mjs +5 -4
- package/dist/components/ui/field.mjs +2 -2
- package/dist/components/ui/financial-cards.js +322 -155
- package/dist/components/ui/financial-cards.mjs +5 -3
- package/dist/components/ui/financial-drawers.js +2 -2
- package/dist/components/ui/financial-drawers.mjs +3 -3
- package/dist/components/ui/financial-sections.js +14 -10
- package/dist/components/ui/financial-sections.mjs +6 -5
- package/dist/components/ui/form-primitives.js +4 -4
- package/dist/components/ui/form-primitives.mjs +3 -3
- package/dist/components/ui/income-bar-chart.js +12 -5
- package/dist/components/ui/income-bar-chart.mjs +5 -4
- package/dist/components/ui/input-group.js +2 -2
- package/dist/components/ui/input-group.mjs +2 -2
- package/dist/components/ui/kanban-column.js +52 -44
- package/dist/components/ui/kanban-column.mjs +7 -5
- package/dist/components/ui/opportunity-card.js +52 -44
- package/dist/components/ui/opportunity-card.mjs +6 -4
- package/dist/components/ui/opportunity-edit-modals.js +1371 -1267
- package/dist/components/ui/opportunity-edit-modals.mjs +10 -10
- package/dist/components/ui/opportunity-summary-tab.js +2748 -2161
- package/dist/components/ui/opportunity-summary-tab.mjs +16 -16
- package/dist/components/ui/page-header.js +92 -0
- package/dist/components/ui/page-header.mjs +8 -0
- package/dist/components/ui/page-top-bar.js +88 -0
- package/dist/components/ui/page-top-bar.mjs +8 -0
- package/dist/components/ui/pagination.js +303 -19
- package/dist/components/ui/pagination.mjs +11 -4
- package/dist/components/ui/pipeline-board.js +209 -195
- package/dist/components/ui/pipeline-board.mjs +10 -8
- package/dist/components/ui/pipeline-dialogs.js +118 -69
- package/dist/components/ui/pipeline-dialogs.mjs +8 -7
- package/dist/components/ui/pipeline-primitives.js +6 -6
- package/dist/components/ui/pipeline-primitives.mjs +2 -2
- package/dist/components/ui/property-cashflow-doughnut-chart.js +14 -12
- package/dist/components/ui/property-cashflow-doughnut-chart.mjs +5 -4
- package/dist/components/ui/property-debt-equity-doughnut-chart.js +14 -12
- package/dist/components/ui/property-debt-equity-doughnut-chart.mjs +5 -4
- package/dist/components/ui/property-mobile-estimate-line-chart.js +16 -14
- package/dist/components/ui/property-mobile-estimate-line-chart.mjs +5 -4
- package/dist/components/ui/sidebar-nav.js +679 -0
- package/dist/components/ui/sidebar-nav.mjs +14 -0
- package/dist/components/ui/stage-timeline.js +6 -6
- package/dist/components/ui/stage-timeline.mjs +3 -3
- package/dist/components/ui/stepper.js +283 -0
- package/dist/components/ui/stepper.mjs +18 -0
- package/dist/components/ui/toggle-group.js +4 -4
- package/dist/components/ui/toggle-group.mjs +2 -2
- package/dist/components/ui/toggle.js +4 -4
- package/dist/components/ui/toggle.mjs +1 -1
- package/dist/components/ui/transactions-expense-categories-doughnut-chart.js +18 -16
- package/dist/components/ui/transactions-expense-categories-doughnut-chart.mjs +5 -4
- package/dist/components/ui/transactions-income-expense-bar-chart.js +28 -12
- package/dist/components/ui/transactions-income-expense-bar-chart.mjs +5 -4
- package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.js +18 -16
- package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.mjs +5 -4
- package/dist/index.js +12927 -8522
- package/dist/index.mjs +288 -190
- package/dist/lib/typography.js +10 -10
- package/dist/lib/typography.mjs +1 -1
- package/dist/styles.css +1 -1
- package/package.json +86 -1
- package/src/components/index.tsx +146 -0
- package/src/components/ui/add-column-modal.tsx +7 -7
- package/src/components/ui/add-lead-modal.tsx +6 -3
- package/src/components/ui/advisor-card.tsx +227 -0
- package/src/components/ui/ai-assistant-drawer.tsx +4 -3
- package/src/components/ui/appointment-action-dialogs.tsx +297 -0
- package/src/components/ui/appointment-availability-settings.tsx +645 -0
- package/src/components/ui/appointment-book-dialog.tsx +618 -0
- package/src/components/ui/appointment-calendar-view.tsx +510 -0
- package/src/components/ui/appointment-detail-sheet.tsx +415 -0
- package/src/components/ui/appointment-gmail-connect.tsx +188 -0
- package/src/components/ui/appointment-mini-card.tsx +104 -0
- package/src/components/ui/appointment-time-slot-picker.tsx +123 -0
- package/src/components/ui/appointment-upcoming-card.tsx +635 -0
- package/src/components/ui/backoffice-alert-history-chart.tsx +10 -2
- package/src/components/ui/backoffice-alerts-chart.tsx +312 -0
- package/src/components/ui/backoffice-connections-chart.tsx +339 -0
- package/src/components/ui/backoffice-contact-history-chart.tsx +10 -2
- package/src/components/ui/badge.tsx +12 -6
- package/src/components/ui/borrowing-capacity-line-chart.tsx +4 -11
- package/src/components/ui/button.tsx +2 -2
- package/src/components/ui/card.tsx +1 -1
- package/src/components/ui/cash-balance-line-chart.tsx +4 -23
- package/src/components/ui/cashflow-bar-chart.tsx +9 -2
- package/src/components/ui/chart-shared.tsx +4 -11
- package/src/components/ui/chip.tsx +23 -19
- package/src/components/ui/color-picker.tsx +309 -0
- package/src/components/ui/data-table.tsx +117 -83
- package/src/components/ui/date-picker.tsx +42 -37
- package/src/components/ui/dialog.tsx +72 -6
- package/src/components/ui/expense-bar-chart.tsx +11 -2
- package/src/components/ui/financial-cards.tsx +99 -10
- package/src/components/ui/income-bar-chart.tsx +11 -2
- package/src/components/ui/opportunity-card.tsx +10 -39
- package/src/components/ui/opportunity-edit-modals.tsx +98 -36
- package/src/components/ui/opportunity-summary-tab.tsx +548 -232
- package/src/components/ui/page-header.tsx +57 -0
- package/src/components/ui/page-top-bar.tsx +48 -0
- package/src/components/ui/pagination.tsx +171 -22
- package/src/components/ui/pipeline-board.tsx +12 -5
- package/src/components/ui/property-cashflow-doughnut-chart.tsx +3 -1
- package/src/components/ui/property-debt-equity-doughnut-chart.tsx +3 -1
- package/src/components/ui/property-mobile-estimate-line-chart.tsx +3 -1
- package/src/components/ui/sidebar-nav.tsx +516 -0
- package/src/components/ui/stepper.tsx +347 -0
- package/src/components/ui/toggle.tsx +4 -4
- package/src/components/ui/transactions-expense-categories-doughnut-chart.tsx +3 -1
- package/src/components/ui/transactions-income-expense-bar-chart.tsx +12 -9
- package/src/components/ui/transactions-liabilities-breakdown-doughnut-chart.tsx +3 -1
- package/src/lib/format-currency.ts +44 -0
- package/src/lib/format-date.ts +50 -0
- package/src/lib/opportunity-constants.ts +12 -0
- package/src/lib/typography.ts +11 -11
- package/src/styles/globals.css +36 -34
- package/src/styles/styles-css.ts +1 -1
- package/tsup.config.ts +17 -0
- package/dist/chunk-PG6K5XEC.mjs +0 -475
- package/dist/chunk-WA6O6EUR.mjs +0 -1885
- package/dist/chunk-WNGWBVLV.mjs +0 -148
- 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
|
+
}
|