@wealthx/shadcn 1.5.22 → 1.5.24

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.
@@ -684,6 +684,15 @@ export interface ChatComposerProps {
684
684
  /** Active reply channel. Defaults to "chat". */
685
685
  channel?: AiConvChannel;
686
686
  onChannelChange?: (channel: AiConvChannel) => void;
687
+ /**
688
+ * The channel this conversation belongs to.
689
+ *
690
+ * - `"chat"` (default) — chat composer shown normally.
691
+ * - `"email"` + `isEmailIntegrated=true` — email tab is active by default.
692
+ * - `"email"` + `isEmailIntegrated=false` — composer is replaced with a
693
+ * prompt to integrate email.
694
+ */
695
+ channelType?: AiConvChannel;
687
696
  /**
688
697
  * When true, the Email tab is shown in the composer. Defaults to false —
689
698
  * consumers must opt in once their tenant's email integration is wired up.
@@ -807,6 +816,7 @@ export function ChatComposer({
807
816
  mode,
808
817
  channel: channelProp = "chat",
809
818
  onChannelChange,
819
+ channelType = "chat",
810
820
  isEmailIntegrated = false,
811
821
  contactEmail = "",
812
822
  inputValue = "",
@@ -817,10 +827,16 @@ export function ChatComposer({
817
827
  onLetAiHandle,
818
828
  className,
819
829
  }: ChatComposerProps) {
830
+ const showIntegrateEmailPrompt =
831
+ channelType === "email" && !isEmailIntegrated;
832
+
820
833
  // Semi-controlled: owns channel state for uncontrolled usage, notifies parent on change.
834
+ // When channelType is email and integrated, default to email tab.
835
+ const initialChannel =
836
+ channelType === "email" && isEmailIntegrated ? "email" : channelProp;
821
837
  // Force chat when email isn't integrated so the panel never lands on a hidden tab.
822
838
  const [channel, setChannel] = React.useState<AiConvChannel>(
823
- isEmailIntegrated ? channelProp : "chat",
839
+ isEmailIntegrated ? initialChannel : "chat",
824
840
  );
825
841
  const [emailTo, setEmailTo] = React.useState(contactEmail);
826
842
  const [emailCc, setEmailCc] = React.useState("");
@@ -850,6 +866,25 @@ export function ChatComposer({
850
866
  onChannelChange?.(c);
851
867
  };
852
868
 
869
+ if (showIntegrateEmailPrompt) {
870
+ return (
871
+ <div
872
+ className={cn(
873
+ "flex flex-col items-center justify-center gap-2 border-t border-border bg-muted/30 px-6 py-8 text-center",
874
+ className,
875
+ )}
876
+ >
877
+ <Mail className="h-8 w-8 text-muted-foreground" />
878
+ <p className="text-sm font-medium text-foreground">
879
+ Email integration required
880
+ </p>
881
+ <p className="text-xs text-muted-foreground">
882
+ Please integrate your email to reply to this conversation.
883
+ </p>
884
+ </div>
885
+ );
886
+ }
887
+
853
888
  return (
854
889
  <div
855
890
  className={cn(
@@ -1030,6 +1065,15 @@ export interface ChatThreadProps {
1030
1065
  /** Active reply channel — "chat" (default) or "email". */
1031
1066
  channel?: AiConvChannel;
1032
1067
  onChannelChange?: (channel: AiConvChannel) => void;
1068
+ /**
1069
+ * The channel this conversation belongs to.
1070
+ *
1071
+ * - `"chat"` (default) — chat composer shown normally.
1072
+ * - `"email"` + `isEmailIntegrated=true` — email tab is active by default.
1073
+ * - `"email"` + `isEmailIntegrated=false` — composer is replaced with a
1074
+ * prompt to integrate email.
1075
+ */
1076
+ channelType?: AiConvChannel;
1033
1077
  /**
1034
1078
  * When true, the Email tab is shown in the composer. Defaults to false.
1035
1079
  */
@@ -1068,6 +1112,7 @@ export function ChatThread({
1068
1112
  isAiTyping = false,
1069
1113
  channel,
1070
1114
  onChannelChange,
1115
+ channelType,
1071
1116
  isEmailIntegrated,
1072
1117
  inputValue,
1073
1118
  onInputChange,
@@ -1310,6 +1355,7 @@ export function ChatThread({
1310
1355
  mode={mode}
1311
1356
  channel={channel}
1312
1357
  onChannelChange={onChannelChange}
1358
+ channelType={channelType}
1313
1359
  isEmailIntegrated={isEmailIntegrated}
1314
1360
  contactEmail={contact.email}
1315
1361
  inputValue={inputValue}
@@ -1818,6 +1864,15 @@ export interface ConversationsPageProps {
1818
1864
  /** Active reply channel — "chat" (default) or "email". */
1819
1865
  channel?: AiConvChannel;
1820
1866
  onChannelChange?: (channel: AiConvChannel) => void;
1867
+ /**
1868
+ * The channel this conversation belongs to.
1869
+ *
1870
+ * - `"chat"` (default) — chat composer shown normally.
1871
+ * - `"email"` + `isEmailIntegrated=true` — email tab is active by default.
1872
+ * - `"email"` + `isEmailIntegrated=false` — composer is replaced with a
1873
+ * prompt to integrate email.
1874
+ */
1875
+ channelType?: AiConvChannel;
1821
1876
  /**
1822
1877
  * When true, the Email tab is shown in the composer. Defaults to false.
1823
1878
  */
@@ -1884,6 +1939,7 @@ export function ConversationsPage({
1884
1939
  onChannelFilterChange,
1885
1940
  channel,
1886
1941
  onChannelChange,
1942
+ channelType,
1887
1943
  isEmailIntegrated,
1888
1944
  inputValue,
1889
1945
  internalNotes,
@@ -1975,6 +2031,7 @@ export function ConversationsPage({
1975
2031
  isAiTyping={isAiTyping}
1976
2032
  channel={channel}
1977
2033
  onChannelChange={onChannelChange}
2034
+ channelType={channelType}
1978
2035
  isEmailIntegrated={isEmailIntegrated}
1979
2036
  inputValue={inputValue}
1980
2037
  onInputChange={onInputChange}
@@ -138,9 +138,25 @@ export interface AppointmentRescheduleDialogProps {
138
138
  currentDate?: string;
139
139
  currentTimeStart?: string;
140
140
  currentTimeEnd?: string;
141
+ /**
142
+ * Fired when the user selects a different date in the calendar.
143
+ * Use this to fetch fresh `amSlots`/`pmSlots` for the new date.
144
+ */
145
+ onDateChange?: (date: Date) => void;
146
+ /**
147
+ * Advisor's weekly availability schedule. Days with `enabled: false` are
148
+ * disabled in the date picker so the user cannot select them.
149
+ */
150
+ schedule?: { day: string; enabled: boolean }[];
151
+ /** True while slots are being fetched for the selected date. */
152
+ isLoadingSlots?: boolean;
141
153
  onReschedule: (date: Date, slot: AppointmentTimeSlot, note: string) => void;
142
154
  }
143
155
 
156
+ const DAY_MAP: Record<string, number> = {
157
+ Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6,
158
+ };
159
+
144
160
  export function AppointmentRescheduleDialog({
145
161
  open,
146
162
  onOpenChange,
@@ -149,12 +165,26 @@ export function AppointmentRescheduleDialog({
149
165
  currentDate,
150
166
  currentTimeStart,
151
167
  currentTimeEnd,
168
+ onDateChange,
169
+ schedule,
170
+ isLoadingSlots,
152
171
  onReschedule,
153
172
  }: AppointmentRescheduleDialogProps) {
154
173
  const [date, setDate] = React.useState<Date | undefined>(new Date());
155
174
  const [slot, setSlot] = React.useState<AppointmentTimeSlot | undefined>();
156
175
  const [note, setNote] = React.useState("");
157
176
 
177
+ const today = React.useMemo(() => new Date(), []);
178
+
179
+ const disabledDayOfWeek = React.useMemo(
180
+ () =>
181
+ schedule
182
+ ?.filter((d) => !d.enabled)
183
+ .map((d) => DAY_MAP[d.day])
184
+ .filter((n): n is number => n !== undefined),
185
+ [schedule],
186
+ );
187
+
158
188
  const handleOpenChange = (next: boolean) => {
159
189
  if (!next) {
160
190
  setDate(new Date());
@@ -181,6 +211,7 @@ export function AppointmentRescheduleDialog({
181
211
 
182
212
  <Separator />
183
213
 
214
+ <div className="flex flex-col gap-4 overflow-y-auto max-h-[calc(90vh-200px)]">
184
215
  {/* Current booking */}
185
216
  {(currentDate || currentTimeStart) && (
186
217
  <div className="flex flex-col gap-1.5">
@@ -215,10 +246,15 @@ export function AppointmentRescheduleDialog({
215
246
  onSelect={(d) => {
216
247
  setDate(d);
217
248
  setSlot(undefined);
249
+ if (d) onDateChange?.(d);
218
250
  }}
219
251
  captionLayout="label"
220
- fromDate={new Date()}
221
- disabled={{ before: new Date() }}
252
+ fromDate={today}
253
+ disabled={
254
+ disabledDayOfWeek && disabledDayOfWeek.length > 0
255
+ ? [{ before: today }, { dayOfWeek: disabledDayOfWeek }]
256
+ : { before: today }
257
+ }
222
258
  className="border border-border"
223
259
  />
224
260
  </div>
@@ -226,26 +262,35 @@ export function AppointmentRescheduleDialog({
226
262
  {/* Time slots */}
227
263
  <div className="flex flex-col gap-5">
228
264
  {date ? (
229
- <>
230
- <div className="flex items-center justify-between">
231
- <p className="text-sm font-semibold">Select a time slot</p>
232
- <span className="text-xs text-muted-foreground">
233
- {totalAvailable} available
234
- </span>
265
+ isLoadingSlots ? (
266
+ <div className="flex h-full flex-col items-center justify-center gap-2 py-8 text-center">
267
+ <p className="text-sm font-semibold">Loading slots…</p>
268
+ <p className="text-xs text-muted-foreground">
269
+ Fetching available times for the selected date.
270
+ </p>
235
271
  </div>
236
- <AppointmentSlotSection
237
- label="Morning"
238
- slots={amSlots}
239
- selectedSlotId={slot?.id}
240
- onSelect={setSlot}
241
- />
242
- <AppointmentSlotSection
243
- label="Afternoon"
244
- slots={pmSlots}
245
- selectedSlotId={slot?.id}
246
- onSelect={setSlot}
247
- />
248
- </>
272
+ ) : (
273
+ <>
274
+ <div className="flex items-center justify-between">
275
+ <p className="text-sm font-semibold">Select a time slot</p>
276
+ <span className="text-xs text-muted-foreground">
277
+ {totalAvailable} available
278
+ </span>
279
+ </div>
280
+ <AppointmentSlotSection
281
+ label="Morning"
282
+ slots={amSlots}
283
+ selectedSlotId={slot?.id}
284
+ onSelect={setSlot}
285
+ />
286
+ <AppointmentSlotSection
287
+ label="Afternoon"
288
+ slots={pmSlots}
289
+ selectedSlotId={slot?.id}
290
+ onSelect={setSlot}
291
+ />
292
+ </>
293
+ )
249
294
  ) : (
250
295
  <div className="flex h-full flex-col items-center justify-center gap-2 py-8 text-center">
251
296
  <p className="text-sm font-semibold">Select a time slot</p>
@@ -274,6 +319,7 @@ export function AppointmentRescheduleDialog({
274
319
  rows={2}
275
320
  />
276
321
  </div>
322
+ </div>
277
323
 
278
324
  <DialogFooter>
279
325
  <DialogClose render={<Button variant="outline" />}>
@@ -87,6 +87,20 @@ export interface AppointmentBookDialogProps {
87
87
  * "Book Appointment".
88
88
  */
89
89
  clients?: AppointmentClient[];
90
+ /**
91
+ * Fired when the user types in the client search field.
92
+ * Use this to fetch clients from an API based on the query.
93
+ * When omitted, clients are filtered locally by name/email.
94
+ */
95
+ onSearchClients?: (query: string) => void;
96
+ /** True while a client search request is in-flight. */
97
+ isSearchingClients?: boolean;
98
+ /** True when more client results can be loaded. */
99
+ hasMoreClients?: boolean;
100
+ /** Fired when the user clicks "Load more" in the client dropdown. */
101
+ onLoadMoreClients?: () => void;
102
+ /** True while a load-more request is in-flight. */
103
+ isLoadingMoreClients?: boolean;
90
104
  /**
91
105
  * Meeting type options. Omit or pass an empty array to hide the meeting
92
106
  * type field (useful in client mode where the type is implicit).
@@ -125,14 +139,25 @@ export interface AppointmentBookDialogProps {
125
139
  */
126
140
  initialClientId?: string;
127
141
  /**
128
- * Pre-set meeting format from the advisor's availability settings.
129
- * In **client mode**, when this is provided the meeting format picker is
130
- * hidden — the client books using this format only.
131
- * Has no effect in advisor mode (advisors always see the format picker).
142
+ * Pre-set meeting format when the dialog opens.
132
143
  *
133
144
  * @remarks Mount-time initialiser only.
134
145
  */
135
146
  defaultMeetingFormat?: AppointmentMeetingFormat;
147
+ /**
148
+ * Which online meeting platform the advisor has integrated.
149
+ *
150
+ * - `"google-meet"` — show only Google Meet
151
+ * - `"microsoft-teams"` — show only MS Teams
152
+ * - `"any"` — show both Google Meet and MS Teams
153
+ *
154
+ * In **client mode**, this replaces the generic "Online Meeting" option
155
+ * with the specific platform(s). In **advisor mode** this has no effect
156
+ * (advisors always see all four formats).
157
+ *
158
+ * When omitted in client mode, the generic "Online Meeting" option is shown.
159
+ */
160
+ onlinePlatform?: "google-meet" | "microsoft-teams" | "any";
136
161
  /**
137
162
  * Guest identity for **client / public booking mode** only.
138
163
  * When all fields are provided the guest form is hidden; when any field is
@@ -173,58 +198,94 @@ function ClientSearch({
173
198
  clients,
174
199
  value,
175
200
  onValueChange,
201
+ onSearch,
202
+ isSearching,
203
+ hasMore,
204
+ onLoadMore,
205
+ isLoadingMore,
176
206
  }: {
177
207
  clients: AppointmentClient[];
178
208
  value: string | undefined;
179
209
  onValueChange: (id: string | undefined) => void;
210
+ onSearch?: (query: string) => void;
211
+ isSearching?: boolean;
212
+ hasMore?: boolean;
213
+ onLoadMore?: () => void;
214
+ isLoadingMore?: boolean;
180
215
  }) {
181
216
  const [query, setQuery] = React.useState("");
182
217
  const [open, setOpen] = React.useState(false);
183
218
  const selected = clients.find((c) => c.id === value);
184
- const filtered = clients.filter((c) => {
185
- const q = query.toLowerCase();
186
- return (
187
- c.name.toLowerCase().includes(q) || c.email.toLowerCase().includes(q)
188
- );
189
- });
219
+
220
+ const filtered = onSearch
221
+ ? clients
222
+ : clients.filter((c) => {
223
+ const q = query.toLowerCase();
224
+ return (
225
+ c.name.toLowerCase().includes(q) || c.email.toLowerCase().includes(q)
226
+ );
227
+ });
190
228
 
191
229
  return (
192
230
  <div className="relative">
193
231
  <Input
194
232
  value={selected ? selected.name : query}
195
233
  onChange={(e) => {
196
- setQuery(e.target.value);
234
+ const v = e.target.value;
235
+ setQuery(v);
197
236
  if (selected) onValueChange(undefined);
198
- setOpen(e.target.value.length > 0);
237
+ setOpen(v.length > 0);
238
+ onSearch?.(v);
199
239
  }}
200
240
  onBlur={() => setTimeout(() => setOpen(false), 150)}
201
241
  placeholder="Search by name or email…"
202
242
  autoComplete="off"
203
243
  />
204
- {open && (filtered.length > 0 || query.length > 0) && (
205
- <div className="absolute z-50 mt-1 w-full border border-border bg-popover shadow-md">
206
- {filtered.length === 0 ? (
244
+ {open && (filtered.length > 0 || query.length > 0 || isSearching) && (
245
+ <div className="absolute z-50 mt-1 max-h-64 w-full overflow-y-auto border border-border bg-popover shadow-md">
246
+ {isSearching ? (
247
+ <p className="px-3 py-6 text-center text-sm text-muted-foreground">
248
+ Searching...
249
+ </p>
250
+ ) : filtered.length === 0 ? (
207
251
  <p className="px-3 py-6 text-center text-sm text-muted-foreground">
208
252
  No clients found.
209
253
  </p>
210
254
  ) : (
211
- filtered.map((c) => (
212
- <Button
213
- key={c.id}
214
- type="button"
215
- variant="ghost"
216
- className="h-auto w-full flex-col items-start gap-0.5 px-3 py-2 text-left hover:bg-primary/5"
217
- onMouseDown={(e) => e.preventDefault()}
218
- onClick={() => {
219
- onValueChange(c.id);
220
- setQuery("");
221
- setOpen(false);
222
- }}
223
- >
224
- <span className="text-sm font-medium">{c.name}</span>
225
- <span className="text-xs text-muted-foreground">{c.email}</span>
226
- </Button>
227
- ))
255
+ <>
256
+ {filtered.map((c) => (
257
+ <Button
258
+ key={c.id}
259
+ type="button"
260
+ variant="ghost"
261
+ className="h-auto w-full flex-col items-start gap-0.5 px-3 py-2 text-left hover:bg-primary/5"
262
+ onMouseDown={(e) => e.preventDefault()}
263
+ onClick={() => {
264
+ onValueChange(c.id);
265
+ setQuery("");
266
+ setOpen(false);
267
+ }}
268
+ >
269
+ <span className="text-sm font-medium">{c.name}</span>
270
+ <span className="text-xs text-muted-foreground">
271
+ {c.email}
272
+ </span>
273
+ </Button>
274
+ ))}
275
+ {hasMore && onLoadMore && (
276
+ <Button
277
+ type="button"
278
+ variant="ghost"
279
+ size="sm"
280
+ className="w-full text-sm text-primary"
281
+ disabled={isLoadingMore}
282
+ onMouseDown={(e) => e.preventDefault()}
283
+ onClick={onLoadMore}
284
+ >
285
+ {isLoadingMore ? "Loading..." : "Load more"}
286
+ </Button>
287
+ )}
288
+ </>
228
289
  )}
229
290
  </div>
230
291
  )}
@@ -277,6 +338,36 @@ const CLIENT_FORMAT_OPTIONS: FormatOption[] = [
277
338
  },
278
339
  ];
279
340
 
341
+ function getFormatOptions(
342
+ platform?: "google-meet" | "microsoft-teams" | "any",
343
+ ): FormatOption[] {
344
+ const call: FormatOption = {
345
+ value: "call",
346
+ label: "Call",
347
+ icon: <Phone className="h-4 w-4" />,
348
+ };
349
+ const googleMeet: FormatOption = {
350
+ value: "google-meet",
351
+ label: "Google Meet",
352
+ icon: <Video className="h-4 w-4" />,
353
+ };
354
+ const msTeams: FormatOption = {
355
+ value: "microsoft-teams",
356
+ label: "MS Teams",
357
+ icon: <Users className="h-4 w-4" />,
358
+ };
359
+ const offline: FormatOption = {
360
+ value: "offline",
361
+ label: "Offline Meeting",
362
+ icon: <MapPin className="h-4 w-4" />,
363
+ };
364
+
365
+ if (platform === "google-meet") return [call, googleMeet, offline];
366
+ if (platform === "microsoft-teams") return [call, msTeams, offline];
367
+ if (platform === "any") return [call, googleMeet, msTeams, offline];
368
+ return [call, offline];
369
+ }
370
+
280
371
  function MeetingFormatSection({
281
372
  format,
282
373
  onFormatChange,
@@ -458,6 +549,11 @@ export function AppointmentBookDialog({
458
549
  open,
459
550
  onOpenChange,
460
551
  clients = [],
552
+ onSearchClients,
553
+ isSearchingClients,
554
+ hasMoreClients,
555
+ onLoadMoreClients,
556
+ isLoadingMoreClients,
461
557
  meetingTypes = [],
462
558
  amSlots,
463
559
  pmSlots,
@@ -468,10 +564,11 @@ export function AppointmentBookDialog({
468
564
  advisorInfo,
469
565
  initialClientId,
470
566
  defaultMeetingFormat,
567
+ onlinePlatform,
471
568
  guestInfo,
472
569
  onBook,
473
570
  }: AppointmentBookDialogProps) {
474
- const isClientMode = clients.length === 0;
571
+ const isClientMode = clients.length === 0 && !onSearchClients;
475
572
  // Guest form is shown in client mode when at least one identity field is missing.
476
573
  const showGuestForm = isClientMode && !(guestInfo?.name && guestInfo?.email);
477
574
 
@@ -620,6 +717,11 @@ export function AppointmentBookDialog({
620
717
  clients={clients}
621
718
  value={clientId}
622
719
  onValueChange={setClientId}
720
+ onSearch={onSearchClients}
721
+ isSearching={isSearchingClients}
722
+ hasMore={hasMoreClients}
723
+ onLoadMore={onLoadMoreClients}
724
+ isLoadingMore={isLoadingMoreClients}
623
725
  />
624
726
  </div>
625
727
  )}
@@ -687,26 +789,21 @@ export function AppointmentBookDialog({
687
789
  </>
688
790
  )}
689
791
 
690
- {/* Format picker — hidden in client mode when broker pre-set a platform */}
691
- {!(isClientMode && defaultMeetingFormat) && (
692
- <div className="flex flex-col gap-1.5">
693
- <Label>Meeting format</Label>
694
- <MeetingFormatSection
695
- format={meetingFormat}
696
- onFormatChange={setMeetingFormat}
697
- offlineLocation={offlineLocation}
698
- onOfflineLocationChange={setOfflineLocation}
699
- customAddress={customAddress}
700
- onCustomAddressChange={setCustomAddress}
701
- advisorOfficeAddress={advisorOfficeAddress}
702
- clientHomeAddress={clientHomeAddress}
703
- isClientMode={isClientMode}
704
- formatOptions={
705
- isClientMode ? CLIENT_FORMAT_OPTIONS : FORMAT_OPTIONS
706
- }
707
- />
708
- </div>
709
- )}
792
+ <div className="flex flex-col gap-1.5">
793
+ <Label>Meeting format</Label>
794
+ <MeetingFormatSection
795
+ format={meetingFormat}
796
+ onFormatChange={setMeetingFormat}
797
+ offlineLocation={offlineLocation}
798
+ onOfflineLocationChange={setOfflineLocation}
799
+ customAddress={customAddress}
800
+ onCustomAddressChange={setCustomAddress}
801
+ advisorOfficeAddress={advisorOfficeAddress}
802
+ clientHomeAddress={clientHomeAddress}
803
+ isClientMode={isClientMode}
804
+ formatOptions={getFormatOptions(onlinePlatform)}
805
+ />
806
+ </div>
710
807
 
711
808
  {/* Phone selection — advisor mode, Call format, client has phones */}
712
809
  {!isClientMode &&
@@ -82,6 +82,18 @@ export interface AppointmentDetailSheetProps {
82
82
  clientProfile?: AppointmentClientProfile;
83
83
  amSlots: AppointmentTimeSlot[];
84
84
  pmSlots: AppointmentTimeSlot[];
85
+ /**
86
+ * Fired when the user selects a different date in the reschedule dialog.
87
+ * Use this to fetch fresh `amSlots`/`pmSlots` for the new date.
88
+ */
89
+ onDateChange?: (date: Date) => void;
90
+ /**
91
+ * Advisor's weekly availability schedule. Days with `enabled: false` are
92
+ * disabled in the reschedule date picker.
93
+ */
94
+ schedule?: { day: string; enabled: boolean }[];
95
+ /** True while slots are being fetched for the selected date. */
96
+ isLoadingSlots?: boolean;
85
97
  onAccept?: (id: string) => void;
86
98
  onDecline?: (id: string) => void;
87
99
  onReschedule?: (
@@ -167,6 +179,9 @@ export function AppointmentDetailSheet({
167
179
  clientProfile,
168
180
  amSlots,
169
181
  pmSlots,
182
+ onDateChange,
183
+ schedule,
184
+ isLoadingSlots,
170
185
  onAccept,
171
186
  onDecline,
172
187
  onReschedule,
@@ -440,6 +455,9 @@ export function AppointmentDetailSheet({
440
455
  currentDate={appointment.date}
441
456
  currentTimeStart={appointment.timeStart}
442
457
  currentTimeEnd={appointment.timeEnd}
458
+ onDateChange={onDateChange}
459
+ schedule={schedule}
460
+ isLoadingSlots={isLoadingSlots}
443
461
  onReschedule={(date, slot, note) => {
444
462
  onReschedule?.(appointment.id, date, slot, note);
445
463
  setRescheduleOpen(false);