@medplum/react-scheduling 5.1.29 → 5.1.31

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.
@@ -2,10 +2,168 @@ import type { Appointment } from '@medplum/fhirtypes';
2
2
  import type { HealthcareService } from '@medplum/fhirtypes';
3
3
  import type { HealthcareServiceAvailableTime } from '@medplum/fhirtypes';
4
4
  import type { JSX } from 'react';
5
+ import type { Location as Location_2 } from '@medplum/fhirtypes';
6
+ import type { MedplumClient } from '@medplum/core';
7
+ import type { Reference } from '@medplum/fhirtypes';
8
+ import type { Resource } from '@medplum/fhirtypes';
5
9
  import type { Schedule } from '@medplum/fhirtypes';
6
10
  import type { Slot } from '@medplum/fhirtypes';
7
11
  import type { WithId } from '@medplum/core';
8
12
 
13
+ /**
14
+ * One way of holding an appointment: a set of actors whose schedules `$find`
15
+ * intersects in a single request. A role contributes as many actors as were
16
+ * chosen for it, since everything chosen attends.
17
+ */
18
+ export declare interface ActorCombination {
19
+ /** Matches `getActorGroupKey` of the appointments offered for these actors. */
20
+ readonly key: string;
21
+ readonly label: string;
22
+ readonly actors: readonly SchedulingActor[];
23
+ readonly schedules: readonly Reference<Schedule>[];
24
+ }
25
+
26
+ /**
27
+ * What an appointment is being asked for: the schedules chosen, per role.
28
+ * Everything named attends.
29
+ */
30
+ export declare type ActorSelections = Partial<Record<SchedulingRole, readonly ScheduleCandidate[]>>;
31
+
32
+ export declare function addDays(date: Date, days: number): Date;
33
+
34
+ /**
35
+ * Chooses the actors an appointment is held on, for one role.
36
+ *
37
+ * Everything chosen attends: `$find` intersects the schedules behind them,
38
+ * so naming a second actor narrows the times to the ones both are free for.
39
+ *
40
+ * Schedules are searched for as the name is typed (via `AsyncAutocomplete`).
41
+ *
42
+ * @param props - The React props.
43
+ * @returns The field for one role.
44
+ */
45
+ export declare function AppointmentActorSelect(props: AppointmentActorSelectProps): JSX.Element;
46
+
47
+ export declare interface AppointmentActorSelectProps {
48
+ /** The role being filled. */
49
+ readonly role: SchedulingRole;
50
+ /** The service being booked. Nothing is offered until it resolves. */
51
+ readonly service: Reference<HealthcareService> | WithId<HealthcareService> | undefined;
52
+ /**
53
+ * The site being booked at. Actors sited elsewhere are left out: a room or a
54
+ * device anywhere inside it counts, a provider only if one of their
55
+ * PractitionerRoles names it.
56
+ */
57
+ readonly location?: Reference<Location_2> | WithId<Location_2>;
58
+ readonly defaultValue?: readonly ScheduleCandidate[];
59
+ readonly onChange: (candidates: readonly ScheduleCandidate[]) => void;
60
+ readonly error?: string;
61
+ readonly disabled?: boolean;
62
+ }
63
+
64
+ export declare interface AppointmentDay {
65
+ /** `YYYY-MM-DD` in the scheduling timezone. */
66
+ readonly key: string;
67
+ /** Local midnight of the same calendar day, for the calendar and its heading. */
68
+ readonly date: Date;
69
+ readonly groups: readonly AppointmentSlotGroup[];
70
+ }
71
+
72
+ /**
73
+ * Shows one day's available times, a card per set of actors offering them.
74
+ *
75
+ * Holds no calendar of its own, so that a view wanting only a list of times can
76
+ * render it alone.
77
+ *
78
+ * @param props - The React props.
79
+ * @returns The day's heading and its times.
80
+ */
81
+ export declare function AppointmentDayTimes(props: AppointmentDayTimesProps): JSX.Element;
82
+
83
+ export declare interface AppointmentDayTimesProps {
84
+ /** Local midnight of the day being shown, which titles it. */
85
+ readonly date: Date;
86
+ /** The times offered on that day, empty for a day that offers none. */
87
+ readonly groups: readonly AppointmentSlotGroup[];
88
+ readonly onSelectAppointment: (appointment: Appointment) => void;
89
+ /** IANA timezone the times are read in. Defaults to the browser's. */
90
+ readonly timezone?: string;
91
+ readonly selected?: Appointment;
92
+ }
93
+
94
+ /**
95
+ * One option in an appointment pick list: what it is called, over what tells it
96
+ * apart from the others.
97
+ * @param props - The React props.
98
+ * @returns The row.
99
+ */
100
+ export declare function AppointmentOptionRow(props: AppointmentOptionRowProps): JSX.Element;
101
+
102
+ export declare interface AppointmentOptionRowProps {
103
+ readonly label: string;
104
+ /** What tells this option apart from one of the same name. Omitted when there is nothing on file. */
105
+ readonly detail?: string;
106
+ }
107
+
108
+ /**
109
+ * Chooses the service an appointment is for.
110
+ *
111
+ * Only services configured for scheduling are offered. A `HealthcareService`
112
+ * without a `SchedulingParameters` extension has no duration or alignment for
113
+ * `$find` to work from, so booking against it cannot succeed.
114
+ *
115
+ * @param props - The React props.
116
+ * @returns The service field.
117
+ */
118
+ export declare function AppointmentServiceSelect(props: AppointmentServiceSelectProps): JSX.Element;
119
+
120
+ export declare interface AppointmentServiceSelectProps {
121
+ readonly defaultValue?: WithId<HealthcareService>;
122
+ readonly onChange: (service: WithId<HealthcareService> | undefined) => void;
123
+ /** A chosen site, which narrows the services on offer to the ones held there. */
124
+ readonly location?: WithId<Location_2> | Reference<Location_2>;
125
+ readonly label?: string;
126
+ readonly error?: string;
127
+ readonly disabled?: boolean;
128
+ }
129
+
130
+ /** A calendar day's worth of available times, split by the actors offering them. */
131
+ export declare interface AppointmentSlotGroup {
132
+ /** Stable key derived from the actors, so React keys survive a refetch. */
133
+ readonly key: string;
134
+ readonly actors: readonly SchedulingActor[];
135
+ readonly durationMinutes: number;
136
+ /** Sorted by start time. */
137
+ readonly appointments: readonly Appointment[];
138
+ }
139
+
140
+ /**
141
+ * One card of available times, headed by the actors offering them.
142
+ * @param props - The React props.
143
+ * @returns The card.
144
+ */
145
+ export declare function AppointmentSlotGroupCard(props: AppointmentSlotGroupCardProps): JSX.Element;
146
+
147
+ export declare interface AppointmentSlotGroupCardProps {
148
+ readonly group: AppointmentSlotGroup;
149
+ readonly onSelectAppointment: (appointment: Appointment) => void;
150
+ /** IANA timezone the times are shown in. Defaults to the browser's. */
151
+ readonly timezone?: string;
152
+ readonly selected?: Appointment;
153
+ readonly disabled?: boolean;
154
+ }
155
+
156
+ /**
157
+ * Actor types whose schedules may be offered for booking.
158
+ *
159
+ * Does not include `PractitionerRole` to prevent double-booking
160
+ * a `Practitioner` who holds multiple roles. See `getSchedulingRole` for how
161
+ * `PractitionerRole` is still used to determine eligibility for a schedule.
162
+ */
163
+ export declare const BOOKABLE_ACTOR_TYPES: readonly ["Practitioner", "Location", "Device"];
164
+
165
+ export declare type BookableActorType = (typeof BOOKABLE_ACTOR_TYPES)[number];
166
+
9
167
  export declare function Calendar(props: CalendarProps): JSX.Element;
10
168
 
11
169
  export declare interface CalendarProps {
@@ -17,6 +175,7 @@ export declare interface CalendarProps {
17
175
  onDoubleClickAppointment?: (appointment: Appointment) => void;
18
176
  onRangeChange?: (range: DateTimeRange) => void;
19
177
  className?: string;
178
+ availableTime?: HealthcareServiceAvailableTime[];
20
179
  }
21
180
 
22
181
  declare interface CommonProps {
@@ -33,11 +192,158 @@ declare interface CommonProps {
33
192
  readonly onCancel?: () => void;
34
193
  }
35
194
 
195
+ /** The days a search covers. Either end may be open. */
196
+ export declare interface DateRange {
197
+ readonly start?: Date;
198
+ readonly end?: Date;
199
+ }
200
+
36
201
  export declare type DateTimeRange = {
37
202
  start: Date;
38
203
  end: Date;
39
204
  };
40
205
 
206
+ /**
207
+ * Returns the last instant of a day, so that a range covers the whole of it.
208
+ * @param date - Any instant during the day.
209
+ * @returns Local midnight less a millisecond, at the end of that day.
210
+ */
211
+ export declare function endOfDay(date: Date): Date;
212
+
213
+ /**
214
+ * Returns the last instant of a date's month.
215
+ * @param date - Any instant during the month.
216
+ * @returns The close of that month's last day.
217
+ */
218
+ export declare function endOfMonth(date: Date): Date;
219
+
220
+ /**
221
+ * Lists the days a range covers, for marking them on the calendar.
222
+ * @param range - The days asked for.
223
+ * @param limit - The most days to return, so an open-ended range stays bounded.
224
+ * @returns Local midnight of each day, or an empty array for an open range.
225
+ */
226
+ export declare function enumerateDateRange(range: DateRange, limit?: number): Date[];
227
+
228
+ /**
229
+ * Restricts appointments to a half of the day, read in the scheduling timezone.
230
+ * @param appointments - Appointments to filter.
231
+ * @param timeOfDay - The half of the day to keep, or `any` to keep all.
232
+ * @param timezone - IANA timezone identifier. Defaults to the browser's.
233
+ * @returns The matching appointments.
234
+ */
235
+ export declare function filterByTimeOfDay(appointments: readonly Appointment[], timeOfDay: TimeOfDay, timezone?: string): Appointment[];
236
+
237
+ /**
238
+ * Narrows candidates to the ones available at one location.
239
+ *
240
+ * A candidate that says nothing about where it is, or whose ancestry cannot be
241
+ * read, is kept: hiding something the caller may be entitled to book is worse
242
+ * than offering something at the wrong site.
243
+ *
244
+ * @param medplum - The Medplum client.
245
+ * @param candidates - Candidates to narrow.
246
+ * @param location - The site being booked at, or undefined to keep everything.
247
+ * @param options - Abort signal.
248
+ * @returns The candidates, minus the ones sited elsewhere.
249
+ */
250
+ export declare function filterCandidatesByLocation(medplum: MedplumClient, candidates: readonly ScheduleCandidate[], location: Reference<Location_2> | WithId<Location_2> | undefined, options?: FilterCandidatesOptions): Promise<ScheduleCandidate[]>;
251
+
252
+ export declare interface FilterCandidatesOptions {
253
+ readonly signal?: AbortSignal;
254
+ }
255
+
256
+ /**
257
+ * Says in words which days a search covers.
258
+ * @param range - The days asked for.
259
+ * @returns The range as a phrase, or undefined when both ends are open.
260
+ */
261
+ export declare function formatDateRange(range: DateRange): string | undefined;
262
+
263
+ /**
264
+ * Formats a calendar day as a heading (e.g. "Monday, July 27").
265
+ * @param date - Local midnight of the day.
266
+ * @returns The formatted day.
267
+ */
268
+ export declare function formatDayHeading(date: Date): string;
269
+
270
+ /**
271
+ * Formats an instant's time of day in a given timezone (e.g. "12:30 PM").
272
+ * @param date - The instant to format.
273
+ * @param timezone - IANA timezone identifier. Defaults to the browser's.
274
+ * @returns The formatted time.
275
+ */
276
+ export declare function formatZonedTime(date: Date, timezone?: string): string;
277
+
278
+ /**
279
+ * Builds the sets of actors an appointment could be held on.
280
+ *
281
+ * One combination is one `$find` request: the schedules within it are
282
+ * intersected, so its times are the times all of those actors are free.
283
+ *
284
+ * @param selections - What has been chosen.
285
+ * @returns One combination holding every chosen actor, in role order, or an
286
+ * empty list when nothing is chosen.
287
+ */
288
+ export declare function getActorCombinations(selections: ActorSelections): ActorCombination[];
289
+
290
+ /**
291
+ * Builds a key identifying the set of actors an appointment is offered by.
292
+ * @param appointment - The proposed appointment.
293
+ * @returns A key that is stable across refetches.
294
+ */
295
+ export declare function getActorGroupKey(appointment: Appointment): string;
296
+
297
+ /**
298
+ * Names the role an actor is filling, from its own reference.
299
+ *
300
+ * Needed where an actor is shown away from the field it was chosen in, which is
301
+ * the only thing that would otherwise say which role it answers.
302
+ *
303
+ * @param actor - A reference to a scheduling actor.
304
+ * @returns The role's label, or undefined for a reference of another type.
305
+ */
306
+ export declare function getActorRoleLabel(actor: Reference): string | undefined;
307
+
308
+ /**
309
+ * Builds a key identifying a set of actors, independent of their order.
310
+ * @param actors - The actors to key.
311
+ * @returns The key.
312
+ */
313
+ export declare function getActorsKey(actors: readonly Reference[]): string;
314
+
315
+ /**
316
+ * Returns a key uniquely identifying a proposed appointment.
317
+ *
318
+ * Proposed appointments have no id, and consecutive pages meet at the boundary
319
+ * of a day, so identity is the times plus the actors.
320
+ *
321
+ * @param appointment - The proposed appointment.
322
+ * @returns The de-duplication key.
323
+ */
324
+ export declare function getAppointmentKey(appointment: Appointment): string;
325
+
326
+ /**
327
+ * Names a candidate's actor, for use in plain-text option lists.
328
+ * @param candidate - The candidate to name.
329
+ * @returns The name to show.
330
+ */
331
+ export declare function getCandidateDisplay(candidate: ScheduleCandidate): string;
332
+
333
+ /**
334
+ * Returns the role a candidate fills, from the type of its actor.
335
+ * @param candidate - The candidate to read.
336
+ * @returns The role, or undefined for an actor of a type nothing books against.
337
+ */
338
+ export declare function getCandidateRole(candidate: ScheduleCandidate): SchedulingRole | undefined;
339
+
340
+ /**
341
+ * Returns an appointment's length in whole minutes.
342
+ * @param appointment - The proposed appointment.
343
+ * @returns The length in minutes, or 0 when either end is missing.
344
+ */
345
+ export declare function getDurationMinutes(appointment: Appointment | undefined): number;
346
+
41
347
  /**
42
348
  * Resolves the availability in effect for a HealthcareService, on a given calendar or on its own.
43
349
  * Hours the Schedule sets for the service take precedence over the service default.
@@ -60,6 +366,93 @@ export declare type DateTimeRange = {
60
366
  */
61
367
  export declare function getEffectiveAvailability(service: WithId<HealthcareService> | undefined, schedule?: Schedule): HealthcareServiceAvailableTime[] | undefined;
62
368
 
369
+ /**
370
+ * Returns the input type to use for a date or time field.
371
+ *
372
+ * JSDOM does not fire change events for `<input type="date">` or
373
+ * `<input type="time">`, so tests get a plain text field, matching what
374
+ * `DateTimeInput` does.
375
+ *
376
+ * @param type - The native input type to use outside of tests.
377
+ * @returns The input type for the current environment.
378
+ */
379
+ export declare function getNativeInputType(type: 'date' | 'time'): string;
380
+
381
+ /**
382
+ * Returns the role an actor type is chosen as.
383
+ * @param actorType - A `Schedule.actor` resource type.
384
+ * @returns The role that actor fills.
385
+ */
386
+ export declare function getSchedulingRole(actorType: SchedulingActorType): SchedulingRole;
387
+
388
+ /**
389
+ * Returns everything chosen, across roles.
390
+ * @param selections - What has been chosen.
391
+ * @returns The chosen candidates, in `SCHEDULING_ROLES` order.
392
+ */
393
+ export declare function getSelectedCandidates(selections: ActorSelections): ScheduleCandidate[];
394
+
395
+ /**
396
+ * Reports why the current selections cannot be searched, if they cannot.
397
+ * @param selections - What has been chosen.
398
+ * @returns A message to show the user, or undefined when the search can run.
399
+ */
400
+ export declare function getSelectionError(selections: ActorSelections): string | undefined;
401
+
402
+ /**
403
+ * Groups proposed appointments into days, and each day into the sets of actors
404
+ * offering those times.
405
+ *
406
+ * @param appointments - Proposed appointments from `$find`.
407
+ * @param timezone - IANA timezone identifier. Defaults to the browser's.
408
+ * @returns Days in ascending order, each holding its groups.
409
+ */
410
+ export declare function groupAppointmentsByDay(appointments: readonly Appointment[], timezone?: string): AppointmentDay[];
411
+
412
+ export declare function isBookableActorType(value: string | undefined): value is BookableActorType;
413
+
414
+ /**
415
+ * Reports whether a role has to be filled.
416
+ * @param role - The role being filled.
417
+ * @returns Whether a search can run without it.
418
+ */
419
+ export declare function isRoleRequired(role: SchedulingRole): boolean;
420
+
421
+ /**
422
+ * Returns whether two instants fall on the same local day.
423
+ * @param left - The first instant.
424
+ * @param right - The second, or undefined when there is nothing to compare.
425
+ * @returns True when both fall on the same local day.
426
+ */
427
+ export declare function isSameDay(left: Date, right: Date | undefined): boolean;
428
+
429
+ export declare function isSchedulingActorType(value: string | undefined): value is SchedulingActorType;
430
+
431
+ /**
432
+ * The longest window `Appointment/$find` accepts. Requests wider than this are
433
+ * rejected outright, so callers have to keep their own date range inside it.
434
+ */
435
+ export declare const MAX_FIND_WINDOW_DAYS = 31;
436
+
437
+ /**
438
+ * Converts a `YYYY-MM-DD` key into local midnight of that calendar day.
439
+ * @param key - A `YYYY-MM-DD` day key.
440
+ * @returns Local midnight of that day.
441
+ */
442
+ export declare function parseDayKey(key: string): Date;
443
+
444
+ /**
445
+ * Reads a wall-clock time on a given day as an instant in a timezone.
446
+ *
447
+ * @param day - Local midnight of the day the time falls on.
448
+ * @param time - A 24-hour `HH:MM` time.
449
+ * @param timezone - IANA timezone identifier. Defaults to the browser's.
450
+ * @returns The instant, or undefined when the time is not a valid `HH:MM`.
451
+ */
452
+ export declare function parseZonedTime(day: Date, time: string, timezone?: string): Date | undefined;
453
+
454
+ export declare const ROLE_LABELS: Record<SchedulingRole, string>;
455
+
63
456
  /**
64
457
  * Edits weekly availability for one visit service type, either as a Schedule's
65
458
  * override of the service hours or as the service's own default hours.
@@ -78,6 +471,16 @@ export declare function ScheduleAvailabilityEditor(props: ScheduleAvailabilityEd
78
471
  */
79
472
  export declare type ScheduleAvailabilityEditorProps = ScheduleOverrideEditorProps | ServiceDefaultEditorProps;
80
473
 
474
+ /**
475
+ * A Schedule that can be booked for a service, paired with the actor it belongs
476
+ * to.
477
+ */
478
+ export declare interface ScheduleCandidate {
479
+ readonly schedule: WithId<Schedule>;
480
+ /** The actor itself, when the search was able to include it. */
481
+ readonly actorResource: WithId<Resource> | undefined;
482
+ }
483
+
81
484
  /**
82
485
  * Props for editing the availability override a Schedule holds for one service.
83
486
  * @param schedule - The Schedule holding the availability override. Must have exactly one actor, as scheduling requires.
@@ -90,6 +493,54 @@ export declare interface ScheduleOverrideEditorProps extends CommonProps {
90
493
  readonly onSave: (updatedSchedule: Schedule) => void | Promise<void>;
91
494
  }
92
495
 
496
+ /** Resource types that may appear in `Schedule.actor`. */
497
+ export declare const SCHEDULING_ACTOR_TYPES: readonly ["Practitioner", "PractitionerRole", "Location", "Device"];
498
+
499
+ /**
500
+ * The parts of an appointment a user chooses, in the order they are asked about.
501
+ *
502
+ * Each role becomes one question, because `$find` intersects the schedules it is
503
+ * given: a time is offered only when the provider *and* the room *and* the
504
+ * device are all free for it.
505
+ */
506
+ export declare const SCHEDULING_ROLES: readonly ["provider", "room", "device"];
507
+
508
+ /**
509
+ * A reference to something a Schedule belongs to. The same union an Appointment
510
+ * accepts as a participant, so an actor can be carried straight across.
511
+ */
512
+ export declare type SchedulingActor = Schedule['actor'][number];
513
+
514
+ export declare type SchedulingActorType = (typeof SCHEDULING_ACTOR_TYPES)[number];
515
+
516
+ export declare type SchedulingRole = (typeof SCHEDULING_ROLES)[number];
517
+
518
+ /**
519
+ * Finds the Schedules that can be booked for one role of a HealthcareService,
520
+ * narrowed to the actors whose name matches what was typed.
521
+ * @param medplum - The Medplum client.
522
+ * @param service - The HealthcareService being booked.
523
+ * @param options - The role, the text typed, the site, an abort signal, and a page size.
524
+ * @returns The matching schedules, each with its actor, by display name.
525
+ */
526
+ export declare function searchScheduleCandidates(medplum: MedplumClient, service: WithId<HealthcareService>, options: SearchScheduleCandidatesOptions): Promise<ScheduleCandidate[]>;
527
+
528
+ export declare interface SearchScheduleCandidatesOptions {
529
+ /** Which of the service's actors to offer. */
530
+ readonly role: SchedulingRole;
531
+ /** What the user typed. Empty offers whatever the role has, unfiltered by name. */
532
+ readonly query: string;
533
+ /**
534
+ * The site being booked at. Actors sited elsewhere are left out: a room or a
535
+ * device anywhere inside it counts, a provider only if one of their
536
+ * PractitionerRoles names it.
537
+ */
538
+ readonly location?: Reference<Location_2> | WithId<Location_2>;
539
+ readonly signal?: AbortSignal;
540
+ /** Maximum schedules to consider. Defaults to 25. */
541
+ readonly count?: number;
542
+ }
543
+
93
544
  /**
94
545
  * Props for editing a service's own default hours, in place of any one calendar's override.
95
546
  * @param schedule - Omitted, which is what selects this mode.
@@ -125,4 +576,36 @@ export declare interface ServiceDefaultEditorProps extends CommonProps {
125
576
  */
126
577
  export declare function setScheduleAvailability(schedule: Schedule, service: WithId<HealthcareService>, availableTime: HealthcareServiceAvailableTime[]): Schedule;
127
578
 
579
+ export declare type TimeOfDay = 'any' | 'morning' | 'afternoon';
580
+
581
+ /**
582
+ * Searches for the times an appointment could be held at.
583
+ * @param options - The service, actor combinations, days, and page size.
584
+ * @returns The times offered, plus load and error state.
585
+ */
586
+ export declare function useProposedAppointments(options: UseProposedAppointmentsOptions): UseProposedAppointmentsResult;
587
+
588
+ export declare interface UseProposedAppointmentsOptions {
589
+ /** The service being booked, as a reference or the resource itself. */
590
+ readonly service: Reference<HealthcareService> | WithId<HealthcareService> | undefined;
591
+ /** The sets of actors to search for, from `getActorCombinations`. */
592
+ readonly combinations: readonly ActorCombination[];
593
+ /** The days to search. Both ends are needed; `$find` refuses an open range. */
594
+ readonly range: DateRange;
595
+ /** Times to ask for per combination. Defaults to 20. */
596
+ readonly count?: number;
597
+ }
598
+
599
+ export declare interface UseProposedAppointmentsResult {
600
+ /**
601
+ * Every time offered, never persisted — `$find` proposes.
602
+ */
603
+ readonly appointments: readonly Appointment[];
604
+ /** How many `$find` requests the current combinations take. */
605
+ readonly requestCount: number;
606
+ readonly loading: boolean;
607
+ /** Set only when every combination failed. */
608
+ readonly error: Error | undefined;
609
+ }
610
+
128
611
  export { }