@wealthx/shadcn 1.5.22 → 1.5.23
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 +78 -78
- package/CHANGELOG.md +6 -0
- package/dist/{chunk-NEMWMXGL.mjs → chunk-3HFOSFOM.mjs} +76 -48
- package/dist/{chunk-F3MIRXRF.mjs → chunk-ONYADWSO.mjs} +7 -1
- package/dist/{chunk-SET6GFGL.mjs → chunk-ZSMQZ3VN.mjs} +96 -73
- package/dist/components/ui/appointment-action-dialogs.js +96 -73
- package/dist/components/ui/appointment-action-dialogs.mjs +1 -1
- package/dist/components/ui/appointment-book-dialog.js +76 -48
- package/dist/components/ui/appointment-book-dialog.mjs +1 -1
- package/dist/components/ui/appointment-detail-sheet.js +102 -73
- package/dist/components/ui/appointment-detail-sheet.mjs +2 -2
- package/dist/index.js +180 -123
- package/dist/index.mjs +3 -3
- package/package.json +1 -1
- package/src/components/ui/appointment-action-dialogs.tsx +67 -21
- package/src/components/ui/appointment-book-dialog.tsx +150 -53
- package/src/components/ui/appointment-detail-sheet.tsx +18 -0
|
@@ -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
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
234
|
+
const v = e.target.value;
|
|
235
|
+
setQuery(v);
|
|
197
236
|
if (selected) onValueChange(undefined);
|
|
198
|
-
setOpen(
|
|
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
|
-
{
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
<
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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);
|