@wealthx/shadcn 1.5.31 → 1.5.33
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 +101 -101
- package/CHANGELOG.md +12 -0
- package/dist/{chunk-SYJ6LVJ6.mjs → chunk-3ZU5BH6X.mjs} +1 -1
- package/dist/{chunk-FTQ2AKZ2.mjs → chunk-4QTHK7ML.mjs} +1 -1
- package/dist/{chunk-T5HU4S4X.mjs → chunk-C7ZTZTEW.mjs} +1 -1
- package/dist/{chunk-KI57CBJR.mjs → chunk-DQNNP6I4.mjs} +33 -24
- package/dist/{chunk-AE4JKISB.mjs → chunk-EEI4FLEE.mjs} +1 -1
- package/dist/{chunk-IEQX4UVP.mjs → chunk-EY36WDCF.mjs} +1 -1
- package/dist/{chunk-HB5BKRMH.mjs → chunk-F3CU6KEI.mjs} +11 -1
- package/dist/chunk-H65NB7KI.mjs +182 -0
- package/dist/{chunk-TRM3KIHT.mjs → chunk-ICCPK3J2.mjs} +1 -1
- package/dist/{chunk-KGVVK6OS.mjs → chunk-ORMC3TV3.mjs} +3 -1
- package/dist/{chunk-HSXMTFIM.mjs → chunk-UD5UF5OC.mjs} +1 -1
- package/dist/{chunk-IW33VLL5.mjs → chunk-X3VEDQPO.mjs} +7 -3
- package/dist/{chunk-AAZSLTER.mjs → chunk-XGRSPFFC.mjs} +16 -7
- package/dist/components/ui/about-you-form.js +9 -6
- package/dist/components/ui/about-you-form.mjs +2 -2
- package/dist/components/ui/ai-conversations/index.js +4 -1
- package/dist/components/ui/ai-conversations/index.mjs +2 -2
- package/dist/components/ui/appointment-availability-settings.js +24 -12
- package/dist/components/ui/appointment-availability-settings.mjs +3 -3
- package/dist/components/ui/appointment-book-dialog.js +33 -24
- package/dist/components/ui/appointment-book-dialog.mjs +1 -1
- package/dist/components/ui/appointment-detail-sheet.js +3 -1
- package/dist/components/ui/appointment-detail-sheet.mjs +1 -1
- package/dist/components/ui/appointment-gmail-connect.js +127 -70
- package/dist/components/ui/appointment-gmail-connect.mjs +1 -1
- package/dist/components/ui/backoffice-signup-steps.js +23 -16
- package/dist/components/ui/backoffice-signup-steps.mjs +3 -3
- package/dist/components/ui/bank-statement-generate-dialog.js +12 -9
- package/dist/components/ui/bank-statement-generate-dialog.mjs +3 -3
- package/dist/components/ui/color-picker.js +21 -14
- package/dist/components/ui/color-picker.mjs +2 -2
- package/dist/components/ui/date-picker.js +6 -3
- package/dist/components/ui/date-picker.mjs +2 -2
- package/dist/components/ui/opportunity-edit-modals.js +45 -42
- package/dist/components/ui/opportunity-edit-modals.mjs +3 -3
- package/dist/components/ui/opportunity-summary-tab.js +48 -45
- package/dist/components/ui/opportunity-summary-tab.mjs +4 -4
- package/dist/components/ui/pipeline-dialogs.js +12 -9
- package/dist/components/ui/pipeline-dialogs.mjs +3 -3
- package/dist/components/ui/popover.js +22 -1
- package/dist/components/ui/popover.mjs +3 -1
- package/dist/components/ui/savings-goal-modal.js +11 -8
- package/dist/components/ui/savings-goal-modal.mjs +2 -2
- package/dist/index.js +349 -257
- package/dist/index.mjs +15 -13
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/index.tsx +4 -0
- package/src/components/ui/appointment-availability-settings.tsx +32 -19
- package/src/components/ui/appointment-book-dialog.tsx +52 -73
- package/src/components/ui/appointment-detail-sheet.tsx +3 -1
- package/src/components/ui/appointment-gmail-connect.tsx +89 -29
- package/src/components/ui/color-picker.tsx +12 -4
- package/src/components/ui/popover.tsx +33 -2
- package/src/styles/styles-css.ts +1 -1
- package/dist/chunk-7TMPOZDE.mjs +0 -122
package/package.json
CHANGED
package/src/components/index.tsx
CHANGED
|
@@ -216,6 +216,7 @@ export type {
|
|
|
216
216
|
AppointmentBookingSlot,
|
|
217
217
|
AppointmentClient,
|
|
218
218
|
AppointmentOfflineLocation,
|
|
219
|
+
AppointmentOnlinePlatform,
|
|
219
220
|
} from "./ui/appointment-book-dialog";
|
|
220
221
|
|
|
221
222
|
export { AppointmentCalendarView } from "./ui/appointment-calendar-view";
|
|
@@ -236,6 +237,7 @@ export type {
|
|
|
236
237
|
export { AppointmentGmailConnect } from "./ui/appointment-gmail-connect";
|
|
237
238
|
export type {
|
|
238
239
|
AppointmentGmailConnectProps,
|
|
240
|
+
EmailProvider,
|
|
239
241
|
GmailConnection,
|
|
240
242
|
} from "./ui/appointment-gmail-connect";
|
|
241
243
|
|
|
@@ -951,6 +953,7 @@ export {
|
|
|
951
953
|
PopoverHeader,
|
|
952
954
|
PopoverTitle,
|
|
953
955
|
PopoverDescription,
|
|
956
|
+
PopoverPortalProvider,
|
|
954
957
|
} from "./ui/popover";
|
|
955
958
|
export type {
|
|
956
959
|
PopoverProps,
|
|
@@ -960,6 +963,7 @@ export type {
|
|
|
960
963
|
PopoverHeaderProps,
|
|
961
964
|
PopoverTitleProps,
|
|
962
965
|
PopoverDescriptionProps,
|
|
966
|
+
PopoverPortalProviderProps,
|
|
963
967
|
} from "./ui/popover";
|
|
964
968
|
|
|
965
969
|
export { PageHeader } from "./ui/page-header";
|
|
@@ -88,9 +88,11 @@ export interface AppointmentAvailabilitySettingsProps {
|
|
|
88
88
|
publicHolidays?: AppointmentBlockedDate[];
|
|
89
89
|
onSave?: (
|
|
90
90
|
schedule: AppointmentDaySchedule[],
|
|
91
|
-
prefs: AppointmentAvailabilityPrefs
|
|
91
|
+
prefs: AppointmentAvailabilityPrefs
|
|
92
92
|
) => void;
|
|
93
93
|
onBlockedDatesChange?: (dates: AppointmentBlockedDate[]) => void;
|
|
94
|
+
/** Fired immediately whenever any booking preference value changes (before Save). */
|
|
95
|
+
onPrefsChange?: (prefs: AppointmentAvailabilityPrefs) => void;
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
// ---------------------------------------------------------------------------
|
|
@@ -376,21 +378,22 @@ export function AppointmentAvailabilitySettings({
|
|
|
376
378
|
publicHolidays: publicHolidaysProp,
|
|
377
379
|
onSave,
|
|
378
380
|
onBlockedDatesChange,
|
|
381
|
+
onPrefsChange,
|
|
379
382
|
}: AppointmentAvailabilitySettingsProps) {
|
|
380
383
|
const [schedule, setSchedule] =
|
|
381
384
|
React.useState<AppointmentDaySchedule[]>(initialSchedule);
|
|
382
385
|
|
|
383
386
|
const [meetingDuration, setMeetingDuration] = React.useState(
|
|
384
|
-
prefsProp?.meetingDuration ?? "30"
|
|
387
|
+
prefsProp?.meetingDuration ?? "30"
|
|
385
388
|
);
|
|
386
389
|
const [schedulingBuffer, setSchedulingBuffer] = React.useState(
|
|
387
|
-
prefsProp?.schedulingBuffer ?? "0"
|
|
390
|
+
prefsProp?.schedulingBuffer ?? "0"
|
|
388
391
|
);
|
|
389
392
|
const [maxSlotsPerDay, setMaxSlotsPerDay] = React.useState(
|
|
390
|
-
prefsProp?.maxSlotsPerDay ?? "8"
|
|
393
|
+
prefsProp?.maxSlotsPerDay ?? "8"
|
|
391
394
|
);
|
|
392
395
|
const [timezone, setTimezone] = React.useState(
|
|
393
|
-
prefsProp?.timezone ?? "Australia/Sydney"
|
|
396
|
+
prefsProp?.timezone ?? "Australia/Sydney"
|
|
394
397
|
);
|
|
395
398
|
const [defaultMeetingPlatform, setDefaultMeetingPlatform] = React.useState<
|
|
396
399
|
"google-meet" | "microsoft-teams" | "any"
|
|
@@ -416,7 +419,7 @@ export function AppointmentAvailabilitySettings({
|
|
|
416
419
|
}
|
|
417
420
|
});
|
|
418
421
|
return entries.sort((a, b) => a.date.localeCompare(b.date));
|
|
419
|
-
}
|
|
422
|
+
}
|
|
420
423
|
);
|
|
421
424
|
const [addMoreOpen, setAddMoreOpen] = React.useState(false);
|
|
422
425
|
|
|
@@ -443,11 +446,12 @@ export function AppointmentAvailabilitySettings({
|
|
|
443
446
|
maxSlotsPerDay,
|
|
444
447
|
timezone,
|
|
445
448
|
defaultMeetingPlatform,
|
|
446
|
-
]
|
|
449
|
+
]
|
|
447
450
|
);
|
|
448
451
|
|
|
449
452
|
const saveGuard = React.useRef(false);
|
|
450
453
|
const timeOffGuard = React.useRef(false);
|
|
454
|
+
const prefsChangeGuard = React.useRef(false);
|
|
451
455
|
|
|
452
456
|
React.useEffect(() => {
|
|
453
457
|
if (!saveGuard.current) {
|
|
@@ -458,6 +462,15 @@ export function AppointmentAvailabilitySettings({
|
|
|
458
462
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
459
463
|
}, [schedule, currentPrefs]);
|
|
460
464
|
|
|
465
|
+
React.useEffect(() => {
|
|
466
|
+
if (!prefsChangeGuard.current) {
|
|
467
|
+
prefsChangeGuard.current = true;
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
onPrefsChange?.(currentPrefs);
|
|
471
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
472
|
+
}, [currentPrefs]);
|
|
473
|
+
|
|
461
474
|
React.useEffect(() => {
|
|
462
475
|
if (!timeOffGuard.current) {
|
|
463
476
|
timeOffGuard.current = true;
|
|
@@ -481,17 +494,17 @@ export function AppointmentAvailabilitySettings({
|
|
|
481
494
|
|
|
482
495
|
const toggleDay = (index: number) => {
|
|
483
496
|
setSchedule((prev) =>
|
|
484
|
-
prev.map((d, i) => (i === index ? { ...d, enabled: !d.enabled } : d))
|
|
497
|
+
prev.map((d, i) => (i === index ? { ...d, enabled: !d.enabled } : d))
|
|
485
498
|
);
|
|
486
499
|
};
|
|
487
500
|
|
|
488
501
|
const updateTime = (
|
|
489
502
|
index: number,
|
|
490
503
|
field: "startTime" | "endTime",
|
|
491
|
-
value: string
|
|
504
|
+
value: string
|
|
492
505
|
) => {
|
|
493
506
|
setSchedule((prev) =>
|
|
494
|
-
prev.map((d, i) => (i === index ? { ...d, [field]: value } : d))
|
|
507
|
+
prev.map((d, i) => (i === index ? { ...d, [field]: value } : d))
|
|
495
508
|
);
|
|
496
509
|
};
|
|
497
510
|
|
|
@@ -501,7 +514,7 @@ export function AppointmentAvailabilitySettings({
|
|
|
501
514
|
|
|
502
515
|
const toggleTimeOff = (date: string, enabled: boolean) => {
|
|
503
516
|
setTimeOffEntries((prev) =>
|
|
504
|
-
prev.map((e) => (e.date === date ? { ...e, enabled } : e))
|
|
517
|
+
prev.map((e) => (e.date === date ? { ...e, enabled } : e))
|
|
505
518
|
);
|
|
506
519
|
};
|
|
507
520
|
|
|
@@ -522,7 +535,7 @@ export function AppointmentAvailabilitySettings({
|
|
|
522
535
|
timeStart: entry.timeStart,
|
|
523
536
|
timeEnd: entry.timeEnd,
|
|
524
537
|
},
|
|
525
|
-
].sort((a, b) => a.date.localeCompare(b.date))
|
|
538
|
+
].sort((a, b) => a.date.localeCompare(b.date))
|
|
526
539
|
);
|
|
527
540
|
};
|
|
528
541
|
|
|
@@ -592,7 +605,7 @@ export function AppointmentAvailabilitySettings({
|
|
|
592
605
|
value={meetingDuration}
|
|
593
606
|
onValueChange={(v) => setMeetingDuration(v as string)}
|
|
594
607
|
>
|
|
595
|
-
<SelectTrigger className="w-
|
|
608
|
+
<SelectTrigger className="w-64">
|
|
596
609
|
<SelectValue>
|
|
597
610
|
{(v) => selectLabel(MEETING_DURATION_OPTIONS, v)}
|
|
598
611
|
</SelectValue>
|
|
@@ -615,7 +628,7 @@ export function AppointmentAvailabilitySettings({
|
|
|
615
628
|
value={schedulingBuffer}
|
|
616
629
|
onValueChange={(v) => setSchedulingBuffer(v as string)}
|
|
617
630
|
>
|
|
618
|
-
<SelectTrigger className="w-
|
|
631
|
+
<SelectTrigger className="w-64">
|
|
619
632
|
<SelectValue>
|
|
620
633
|
{(v) => selectLabel(SCHEDULING_BUFFER_OPTIONS, v)}
|
|
621
634
|
</SelectValue>
|
|
@@ -638,7 +651,7 @@ export function AppointmentAvailabilitySettings({
|
|
|
638
651
|
value={maxSlotsPerDay}
|
|
639
652
|
onValueChange={(v) => setMaxSlotsPerDay(v as string)}
|
|
640
653
|
>
|
|
641
|
-
<SelectTrigger className="w-
|
|
654
|
+
<SelectTrigger className="w-64">
|
|
642
655
|
<SelectValue>
|
|
643
656
|
{(v) => selectLabel(MAX_SLOTS_OPTIONS, v)}
|
|
644
657
|
</SelectValue>
|
|
@@ -661,7 +674,7 @@ export function AppointmentAvailabilitySettings({
|
|
|
661
674
|
value={timezone}
|
|
662
675
|
onValueChange={(v) => setTimezone(v as string)}
|
|
663
676
|
>
|
|
664
|
-
<SelectTrigger className="w-
|
|
677
|
+
<SelectTrigger className="w-64">
|
|
665
678
|
<SelectValue>
|
|
666
679
|
{(v) => selectLabel(TIMEZONE_OPTIONS, v)}
|
|
667
680
|
</SelectValue>
|
|
@@ -684,11 +697,11 @@ export function AppointmentAvailabilitySettings({
|
|
|
684
697
|
value={defaultMeetingPlatform}
|
|
685
698
|
onValueChange={(v) =>
|
|
686
699
|
setDefaultMeetingPlatform(
|
|
687
|
-
v as "google-meet" | "microsoft-teams" | "any"
|
|
700
|
+
v as "google-meet" | "microsoft-teams" | "any"
|
|
688
701
|
)
|
|
689
702
|
}
|
|
690
703
|
>
|
|
691
|
-
<SelectTrigger className="w-
|
|
704
|
+
<SelectTrigger className="w-64">
|
|
692
705
|
<SelectValue>
|
|
693
706
|
{(v) => selectLabel(MEETING_PLATFORM_OPTIONS, v)}
|
|
694
707
|
</SelectValue>
|
|
@@ -744,7 +757,7 @@ export function AppointmentAvailabilitySettings({
|
|
|
744
757
|
htmlFor={`toff-${entry.date}`}
|
|
745
758
|
className={cn(
|
|
746
759
|
"cursor-pointer text-sm font-medium",
|
|
747
|
-
!entry.enabled && "text-muted-foreground"
|
|
760
|
+
!entry.enabled && "text-muted-foreground"
|
|
748
761
|
)}
|
|
749
762
|
>
|
|
750
763
|
{entry.label ?? formattedDate}
|
|
@@ -145,19 +145,16 @@ export interface AppointmentBookDialogProps {
|
|
|
145
145
|
*/
|
|
146
146
|
defaultMeetingFormat?: AppointmentMeetingFormat;
|
|
147
147
|
/**
|
|
148
|
-
* Which online meeting platform the advisor has integrated.
|
|
148
|
+
* Which online meeting platform(s) the advisor has integrated.
|
|
149
149
|
*
|
|
150
|
+
* - `"online"` — 1 email connected; show a generic "Online Meeting" button
|
|
150
151
|
* - `"google-meet"` — show only Google Meet
|
|
151
152
|
* - `"microsoft-teams"` — show only MS Teams
|
|
152
|
-
* - `"any"` — show both Google Meet and MS Teams
|
|
153
|
+
* - `"any"` — 2 emails connected; show both Google Meet and MS Teams
|
|
153
154
|
*
|
|
154
|
-
*
|
|
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.
|
|
155
|
+
* When omitted, no online option is shown (advisor has no email integration).
|
|
159
156
|
*/
|
|
160
|
-
onlinePlatform?:
|
|
157
|
+
onlinePlatform?: AppointmentOnlinePlatform;
|
|
161
158
|
/**
|
|
162
159
|
* Guest identity for **client / public booking mode** only.
|
|
163
160
|
* When all fields are provided the guest form is hidden; when any field is
|
|
@@ -303,69 +300,49 @@ interface FormatOption {
|
|
|
303
300
|
icon: React.ReactNode;
|
|
304
301
|
}
|
|
305
302
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
icon: <MapPin className="h-4 w-4" />,
|
|
338
|
-
},
|
|
339
|
-
];
|
|
303
|
+
export type AppointmentOnlinePlatform =
|
|
304
|
+
| "online"
|
|
305
|
+
| "google-meet"
|
|
306
|
+
| "microsoft-teams"
|
|
307
|
+
| "any";
|
|
308
|
+
|
|
309
|
+
const FMT_CALL: FormatOption = {
|
|
310
|
+
value: "call",
|
|
311
|
+
label: "Call",
|
|
312
|
+
icon: <Phone className="h-4 w-4" />,
|
|
313
|
+
};
|
|
314
|
+
const FMT_ONLINE: FormatOption = {
|
|
315
|
+
value: "online",
|
|
316
|
+
label: "Online Meeting",
|
|
317
|
+
icon: <Video className="h-4 w-4" />,
|
|
318
|
+
};
|
|
319
|
+
const FMT_GOOGLE_MEET: FormatOption = {
|
|
320
|
+
value: "google-meet",
|
|
321
|
+
label: "Google Meet",
|
|
322
|
+
icon: <Video className="h-4 w-4" />,
|
|
323
|
+
};
|
|
324
|
+
const FMT_MS_TEAMS: FormatOption = {
|
|
325
|
+
value: "microsoft-teams",
|
|
326
|
+
label: "MS Teams",
|
|
327
|
+
icon: <Users className="h-4 w-4" />,
|
|
328
|
+
};
|
|
329
|
+
const FMT_OFFLINE: FormatOption = {
|
|
330
|
+
value: "offline",
|
|
331
|
+
label: "Offline Meeting",
|
|
332
|
+
icon: <MapPin className="h-4 w-4" />,
|
|
333
|
+
};
|
|
340
334
|
|
|
341
335
|
function getFormatOptions(
|
|
342
|
-
platform?:
|
|
336
|
+
platform?: AppointmentOnlinePlatform
|
|
343
337
|
): FormatOption[] {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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];
|
|
338
|
+
if (platform === "online") return [FMT_CALL, FMT_ONLINE, FMT_OFFLINE];
|
|
339
|
+
if (platform === "google-meet")
|
|
340
|
+
return [FMT_CALL, FMT_GOOGLE_MEET, FMT_OFFLINE];
|
|
341
|
+
if (platform === "microsoft-teams")
|
|
342
|
+
return [FMT_CALL, FMT_MS_TEAMS, FMT_OFFLINE];
|
|
343
|
+
if (platform === "any")
|
|
344
|
+
return [FMT_CALL, FMT_GOOGLE_MEET, FMT_MS_TEAMS, FMT_OFFLINE];
|
|
345
|
+
return [FMT_CALL, FMT_OFFLINE];
|
|
369
346
|
}
|
|
370
347
|
|
|
371
348
|
function MeetingFormatSection({
|
|
@@ -474,7 +451,9 @@ function SummaryRow({
|
|
|
474
451
|
}) {
|
|
475
452
|
return (
|
|
476
453
|
<div
|
|
477
|
-
className={`flex items-center justify-between px-4 py-3 ${
|
|
454
|
+
className={`flex items-center justify-between px-4 py-3 ${
|
|
455
|
+
border ? "border-b border-border" : ""
|
|
456
|
+
}`}
|
|
478
457
|
>
|
|
479
458
|
<span className="text-xs text-muted-foreground">{label}</span>
|
|
480
459
|
{typeof value === "string" ? (
|
|
@@ -578,7 +557,7 @@ export function AppointmentBookDialog({
|
|
|
578
557
|
?.filter((d) => !d.enabled)
|
|
579
558
|
.map((d) => DAY_MAP[d.day])
|
|
580
559
|
.filter((n): n is number => n !== undefined),
|
|
581
|
-
[schedule]
|
|
560
|
+
[schedule]
|
|
582
561
|
);
|
|
583
562
|
|
|
584
563
|
// Stable "today" reference — avoids prop identity churn on CalendarPicker each render
|
|
@@ -590,7 +569,7 @@ export function AppointmentBookDialog({
|
|
|
590
569
|
React.useState<AppointmentMeetingFormat>(defaultMeetingFormat ?? "call");
|
|
591
570
|
const [offlineLocation, setOfflineLocation] =
|
|
592
571
|
React.useState<AppointmentOfflineLocation>(
|
|
593
|
-
advisorOfficeAddress ? "office" : "custom"
|
|
572
|
+
advisorOfficeAddress ? "office" : "custom"
|
|
594
573
|
);
|
|
595
574
|
const [customAddress, setCustomAddress] = React.useState("");
|
|
596
575
|
const [date, setDate] = React.useState<Date | undefined>(() => new Date());
|
|
@@ -599,7 +578,7 @@ export function AppointmentBookDialog({
|
|
|
599
578
|
>(undefined);
|
|
600
579
|
const [notes, setNotes] = React.useState("");
|
|
601
580
|
const [selectedPhone, setSelectedPhone] = React.useState<string | undefined>(
|
|
602
|
-
undefined
|
|
581
|
+
undefined
|
|
603
582
|
);
|
|
604
583
|
|
|
605
584
|
// Guest identity — client / public booking mode only
|
|
@@ -613,7 +592,7 @@ export function AppointmentBookDialog({
|
|
|
613
592
|
|
|
614
593
|
const selectedClient = React.useMemo(
|
|
615
594
|
() => clients.find((c) => c.id === clientId),
|
|
616
|
-
[clients, clientId]
|
|
595
|
+
[clients, clientId]
|
|
617
596
|
);
|
|
618
597
|
|
|
619
598
|
// Pre-select client when dialog opens with an initialClientId (e.g. rebook after cancellation)
|
|
@@ -100,7 +100,7 @@ export interface AppointmentDetailSheetProps {
|
|
|
100
100
|
id: string,
|
|
101
101
|
date: Date,
|
|
102
102
|
slot: AppointmentTimeSlot,
|
|
103
|
-
note: string
|
|
103
|
+
note: string
|
|
104
104
|
) => void;
|
|
105
105
|
/** Called when "Book a New Appointment" is clicked on a cancelled appointment */
|
|
106
106
|
onBookNew?: (clientId?: string) => void;
|
|
@@ -159,6 +159,7 @@ const MEETING_FORMAT_META: Record<
|
|
|
159
159
|
icon: <Users className={ICON_CLASS} />,
|
|
160
160
|
label: "Microsoft Teams",
|
|
161
161
|
},
|
|
162
|
+
online: { icon: <Video className={ICON_CLASS} />, label: "Online Meeting" },
|
|
162
163
|
offline: { icon: <MapPin className={ICON_CLASS} />, label: "In Person" },
|
|
163
164
|
};
|
|
164
165
|
|
|
@@ -166,6 +167,7 @@ const MEETING_FORMAT_META: Record<
|
|
|
166
167
|
const ONLINE_FORMATS = new Set<AppointmentMeetingFormat>([
|
|
167
168
|
"google-meet",
|
|
168
169
|
"microsoft-teams",
|
|
170
|
+
"online",
|
|
169
171
|
]);
|
|
170
172
|
|
|
171
173
|
// ---------------------------------------------------------------------------
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
1
2
|
import { Badge } from "./badge";
|
|
2
3
|
import { Button } from "./button";
|
|
3
4
|
import { Input } from "./input";
|
|
@@ -8,6 +9,8 @@ import { Check, Copy, ExternalLink, Link2, Mail, Settings } from "lucide-react";
|
|
|
8
9
|
// Types
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
|
|
12
|
+
export type EmailProvider = "gmail" | "outlook";
|
|
13
|
+
|
|
11
14
|
export interface GmailConnection {
|
|
12
15
|
connected: boolean;
|
|
13
16
|
email?: string;
|
|
@@ -16,13 +19,20 @@ export interface GmailConnection {
|
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export interface AppointmentGmailConnectProps {
|
|
19
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Email provider that is connected. Determines displayed labels.
|
|
24
|
+
* Defaults to "gmail".
|
|
25
|
+
*/
|
|
26
|
+
provider?: EmailProvider;
|
|
27
|
+
/** Email connection state sourced from Settings > Integrations */
|
|
20
28
|
connection: GmailConnection;
|
|
21
29
|
/** Booking link to display when connected */
|
|
22
30
|
calendarLink?: string;
|
|
31
|
+
/** Highlights the card with a primary-colored border — used when this provider matches the default meeting platform. */
|
|
32
|
+
highlighted?: boolean;
|
|
23
33
|
/** Called when user clicks the copy button next to the calendar link */
|
|
24
34
|
onCopyLink?: () => void;
|
|
25
|
-
/** Called when user clicks "Go to Integrations"
|
|
35
|
+
/** Called when user clicks "Go to Integrations" */
|
|
26
36
|
onGoToIntegrations?: () => void;
|
|
27
37
|
}
|
|
28
38
|
|
|
@@ -30,6 +40,38 @@ export interface AppointmentGmailConnectProps {
|
|
|
30
40
|
// Constants
|
|
31
41
|
// ---------------------------------------------------------------------------
|
|
32
42
|
|
|
43
|
+
function GmailIcon({ className }: { className?: string }) {
|
|
44
|
+
return (
|
|
45
|
+
<svg viewBox="0 0 24 24" className={className} aria-hidden="true">
|
|
46
|
+
<path
|
|
47
|
+
fill="#EA4335"
|
|
48
|
+
d="M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z"
|
|
49
|
+
/>
|
|
50
|
+
</svg>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function OutlookIcon({ className }: { className?: string }) {
|
|
55
|
+
return (
|
|
56
|
+
<svg viewBox="0 0 24 24" className={className} aria-hidden="true">
|
|
57
|
+
<path
|
|
58
|
+
fill="#0078D4"
|
|
59
|
+
d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.9-1.801"
|
|
60
|
+
/>
|
|
61
|
+
</svg>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type IconComponent = (props: { className?: string }) => JSX.Element;
|
|
66
|
+
|
|
67
|
+
const PROVIDER_META: Record<
|
|
68
|
+
EmailProvider,
|
|
69
|
+
{ label: string; iconBg: string; Icon: IconComponent }
|
|
70
|
+
> = {
|
|
71
|
+
gmail: { label: "Gmail", iconBg: "bg-[#EA4335]/10", Icon: GmailIcon },
|
|
72
|
+
outlook: { label: "Outlook", iconBg: "bg-[#0078D4]/10", Icon: OutlookIcon },
|
|
73
|
+
};
|
|
74
|
+
|
|
33
75
|
const PERMISSIONS = [
|
|
34
76
|
"Read your calendar availability",
|
|
35
77
|
"Send appointment confirmation emails",
|
|
@@ -41,43 +83,61 @@ const PERMISSIONS = [
|
|
|
41
83
|
// ---------------------------------------------------------------------------
|
|
42
84
|
|
|
43
85
|
export function AppointmentGmailConnect({
|
|
86
|
+
provider = "gmail",
|
|
44
87
|
connection,
|
|
45
88
|
calendarLink,
|
|
89
|
+
highlighted = false,
|
|
46
90
|
onCopyLink,
|
|
47
91
|
onGoToIntegrations,
|
|
48
92
|
}: AppointmentGmailConnectProps) {
|
|
93
|
+
const { label, iconBg, Icon } = PROVIDER_META[provider];
|
|
94
|
+
|
|
49
95
|
if (connection.connected) {
|
|
50
96
|
return (
|
|
51
|
-
<div
|
|
97
|
+
<div
|
|
98
|
+
className={cn(
|
|
99
|
+
"flex flex-col border bg-card",
|
|
100
|
+
highlighted ? "border-primary" : "border-border"
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
52
103
|
{/* Header */}
|
|
53
|
-
<div className="flex items-
|
|
54
|
-
<div
|
|
55
|
-
|
|
104
|
+
<div className="flex items-start gap-3 px-5 py-4">
|
|
105
|
+
<div
|
|
106
|
+
className={cn(
|
|
107
|
+
"flex h-10 w-10 shrink-0 items-center justify-center",
|
|
108
|
+
iconBg
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
<Icon className="h-5 w-5" />
|
|
56
112
|
</div>
|
|
57
|
-
<div className="
|
|
58
|
-
<p className="text-base font-semibold">
|
|
113
|
+
<div className="min-w-0 flex-1">
|
|
114
|
+
<p className="text-base font-semibold">{label} Connected</p>
|
|
59
115
|
<p className="truncate text-sm text-muted-foreground">
|
|
60
116
|
{connection.email}
|
|
61
117
|
</p>
|
|
62
118
|
</div>
|
|
63
|
-
<
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
119
|
+
<div className="flex shrink-0 flex-col items-end gap-1.5">
|
|
120
|
+
<Button
|
|
121
|
+
size="sm"
|
|
122
|
+
variant="ghost"
|
|
123
|
+
className="gap-1.5 text-muted-foreground"
|
|
124
|
+
onClick={onGoToIntegrations}
|
|
125
|
+
>
|
|
126
|
+
<Settings className="h-3.5 w-3.5" />
|
|
127
|
+
Manage
|
|
128
|
+
</Button>
|
|
129
|
+
<div className="flex items-center gap-2">
|
|
130
|
+
<Badge variant="success">
|
|
131
|
+
<Check size={10} />
|
|
132
|
+
Active
|
|
133
|
+
</Badge>
|
|
134
|
+
{connection.connectedAt && (
|
|
135
|
+
<p className="text-xs text-muted-foreground">
|
|
136
|
+
Connected since {connection.connectedAt}
|
|
137
|
+
</p>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
81
141
|
</div>
|
|
82
142
|
|
|
83
143
|
<Separator />
|
|
@@ -141,10 +201,10 @@ export function AppointmentGmailConnect({
|
|
|
141
201
|
</div>
|
|
142
202
|
|
|
143
203
|
<div className="flex flex-col gap-1.5">
|
|
144
|
-
<p className="text-base font-semibold">
|
|
204
|
+
<p className="text-base font-semibold">{label} not connected</p>
|
|
145
205
|
<p className="max-w-xs text-sm text-muted-foreground">
|
|
146
|
-
Connect your
|
|
147
|
-
and appointment confirmations.
|
|
206
|
+
Connect your {label} account in Integrations to enable calendar
|
|
207
|
+
booking and appointment confirmations.
|
|
148
208
|
</p>
|
|
149
209
|
</div>
|
|
150
210
|
|
|
@@ -107,7 +107,7 @@ function ColorSwatch({
|
|
|
107
107
|
size === "md" ? "size-7" : "size-5",
|
|
108
108
|
selected &&
|
|
109
109
|
"ring-2 ring-foreground ring-offset-1 ring-offset-background",
|
|
110
|
-
className
|
|
110
|
+
className
|
|
111
111
|
)}
|
|
112
112
|
style={{ backgroundColor: color }}
|
|
113
113
|
/>
|
|
@@ -130,16 +130,24 @@ function ColorPickerContent({
|
|
|
130
130
|
presets = COLOR_PICKER_PRESETS,
|
|
131
131
|
}: ColorPickerContentProps) {
|
|
132
132
|
const [hexInput, setHexInput] = React.useState(value);
|
|
133
|
+
const hexInputRef = React.useRef(hexInput);
|
|
134
|
+
hexInputRef.current = hexInput;
|
|
133
135
|
|
|
134
136
|
React.useEffect(() => {
|
|
135
|
-
|
|
137
|
+
// Only sync from external value (e.g. swatch click) when the user's current
|
|
138
|
+
// input doesn't already produce the same color — prevents cursor jump mid-type.
|
|
139
|
+
if (value !== hexInputRef.current) {
|
|
140
|
+
setHexInput(value);
|
|
141
|
+
}
|
|
136
142
|
}, [value]);
|
|
137
143
|
|
|
138
144
|
function handleHexInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
139
145
|
const raw = e.target.value;
|
|
140
146
|
setHexInput(raw);
|
|
141
147
|
const normalized = normalizeHex(raw);
|
|
142
|
-
|
|
148
|
+
// Only commit on a complete 6-digit hex so intermediate 3-digit matches
|
|
149
|
+
// (#abc shorthand) don't fire onChange mid-type and shift the cursor.
|
|
150
|
+
if (/^#[0-9A-Fa-f]{6}$/.test(normalized)) {
|
|
143
151
|
onChange(normalized);
|
|
144
152
|
}
|
|
145
153
|
}
|
|
@@ -271,7 +279,7 @@ function ColorPicker({
|
|
|
271
279
|
"flex h-9 min-w-[140px] cursor-pointer items-center gap-2.5 border border-input bg-background px-3 text-sm outline-none transition-colors",
|
|
272
280
|
"hover:border-ring",
|
|
273
281
|
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
|
|
274
|
-
"disabled:cursor-not-allowed disabled:opacity-50"
|
|
282
|
+
"disabled:cursor-not-allowed disabled:opacity-50"
|
|
275
283
|
)}
|
|
276
284
|
>
|
|
277
285
|
<span
|