@medplum/react-scheduling 5.1.38 → 5.1.39

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.
@@ -9,8 +9,8 @@ import type { MantineThemeColors } from '@mantine/core';
9
9
  import type { MedplumClient } from '@medplum/core';
10
10
  import type { OperationOutcome } from '@medplum/fhirtypes';
11
11
  import type { Patient } from '@medplum/fhirtypes';
12
+ import type { ReactNode } from 'react';
12
13
  import type { Reference } from '@medplum/fhirtypes';
13
- import type { Resource } from '@medplum/fhirtypes';
14
14
  import type { Schedule } from '@medplum/fhirtypes';
15
15
  import type { SchedulingRequirement } from '@medplum/core';
16
16
  import type { Slot } from '@medplum/fhirtypes';
@@ -19,8 +19,8 @@ import type { WithId } from '@medplum/core';
19
19
 
20
20
  /**
21
21
  * One way of holding an appointment: a set of actors whose schedules `$find`
22
- * intersects in a single request. A role contributes as many actors as were
23
- * chosen for it, since everything chosen attends.
22
+ * intersects in a single request. Each requirement contributes exactly one
23
+ * actor, since a row is satisfied by any one of its alternatives.
24
24
  */
25
25
  export declare interface ActorCombination {
26
26
  /** Matches `getActorGroupKey` of the appointments offered for these actors. */
@@ -31,26 +31,70 @@ export declare interface ActorCombination {
31
31
  }
32
32
 
33
33
  /**
34
- * What an appointment is being asked for: the schedules chosen, per actor type.
35
- * Everything named attends.
34
+ * One thing an appointment needs, of a single actor type.
35
+ *
36
+ * The candidates in it are alternatives: a row naming two
37
+ * providers asks for *either* of them rather than both.
36
38
  */
37
- export declare type ActorSelections = Partial<Record<SchedulingActorType, readonly ScheduleCandidate[]>>;
39
+ export declare interface ActorRequirement {
40
+ readonly id: string;
41
+ readonly candidates: readonly ScheduleCandidate[];
42
+ }
43
+
44
+ /**
45
+ * What an appointment is being asked for, per actor type.
46
+ *
47
+ * Each actor type holds a list of requirements, and the two directions read
48
+ * differently: requirements are ANDed, so two provider rows ask for two
49
+ * providers, while the candidates within one row are ORed.
50
+ */
51
+ export declare type ActorSelections = Partial<Record<SchedulingActorType, readonly ActorRequirement[]>>;
38
52
 
39
53
  export declare function addDays(date: Date, days: number): Date;
40
54
 
41
55
  /**
42
- * Chooses the actors an appointment is held on, for one actor type.
56
+ * Chooses which actors of one type can hold an appointment.
43
57
  *
44
- * Everything chosen attends: `$find` intersects the schedules behind them,
45
- * so naming a second actor narrows the times to the ones both are free for.
58
+ * The names in one of these fields are **alternatives** to each other.
59
+ * Several actors that all have to attend go in a field each - see {@link AppointmentActorSelections},
60
+ * which arranges these into rows.
46
61
  *
47
- * Schedules are searched for as the name is typed (via `AsyncAutocomplete`).
62
+ * The results are `Schedule` resources, bound to the actors that can hold the appointment.
48
63
  *
49
64
  * @param props - The React props.
50
65
  * @returns The field for one actor type.
51
66
  */
52
67
  export declare function AppointmentActorSelect(props: AppointmentActorSelectProps): JSX.Element;
53
68
 
69
+ /**
70
+ * Asks what an appointment needs by actor type (e.g., provider, room, device).
71
+ *
72
+ * Each actor type gets a stack of rows. **The rows are ANDed and the names
73
+ * within a row are ORed**: two provider rows ask for two providers, while two
74
+ * names in one row ask for either of them.
75
+ *
76
+ * A row is one alternative set, so it is also one dimension of the search:
77
+ * `getActorCombinations` takes one name from each row, and every way of doing
78
+ * that is one `$find` request.
79
+ *
80
+ * @param props - The React props.
81
+ * @returns One group of rows per bookable actor type.
82
+ */
83
+ export declare function AppointmentActorSelections(props: AppointmentActorSelectionsProps): JSX.Element;
84
+
85
+ export declare interface AppointmentActorSelectionsProps {
86
+ /** What the appointment is being asked for so far, per actor type. */
87
+ readonly value: ActorSelections;
88
+ /** The service being booked. Nothing is offered until it resolves. */
89
+ readonly service: Reference<HealthcareService> | WithId<HealthcareService> | undefined;
90
+ /** The site being booked at. Actors sited elsewhere are left out. */
91
+ readonly location?: Reference<Location_2> | WithId<Location_2>;
92
+ readonly disabled?: boolean;
93
+ /** What is wrong with a type's rows, shown under them. None by default. */
94
+ readonly errors?: Partial<Record<BookableActorType, string>>;
95
+ readonly onChange: (selections: ActorSelections) => void;
96
+ }
97
+
54
98
  export declare interface AppointmentActorSelectProps {
55
99
  readonly actorType: BookableActorType;
56
100
  /** The service being booked. Nothing is offered until it resolves. */
@@ -65,6 +109,16 @@ export declare interface AppointmentActorSelectProps {
65
109
  readonly onChange: (candidates: readonly ScheduleCandidate[]) => void;
66
110
  readonly error?: string;
67
111
  readonly disabled?: boolean;
112
+ /** Names the field. Defaults to the actor type's own label. */
113
+ readonly label?: ReactNode;
114
+ /** The line under the field. None by default. */
115
+ readonly description?: ReactNode;
116
+ /** What the empty field invites. Defaults to searching the actor type by name. */
117
+ readonly placeholder?: string;
118
+ /** Whether the field must be answered. Defaults to whether its actor type is required. */
119
+ readonly required?: boolean;
120
+ /** Whether to mark the field with an asterisk. Defaults to `required`. */
121
+ readonly withAsterisk?: boolean;
68
122
  }
69
123
 
70
124
  /** What a booking wrote, as `Appointment/$book` returned it. */
@@ -149,13 +203,6 @@ export declare interface AppointmentOptionRowProps {
149
203
  * Gathers what a visit is held on, finds a time every one of them is free, and
150
204
  * hands the proposal out to be booked.
151
205
  *
152
- * One field per scheduling role, each searching the schedules bookable for the
153
- * chosen visit type. Everything named attends, because `$find` intersects their
154
- * schedules — so naming a second room narrows the times rather than widening them.
155
- *
156
- * Only a time the search offered can be chosen: the field holding it accepts no
157
- * input, so nothing can be booked onto time nobody checked availability for.
158
- *
159
206
  * Writes nothing and announces nothing: `onBook` owns that. Mount this to do
160
207
  * something other than `$book` with the proposal — hold it through `$hold`, or
161
208
  * write it inside a transaction of your own. {@link AppointmentBookingForm} is the
@@ -246,7 +293,7 @@ export declare interface AppointmentServiceSelectProps {
246
293
  export declare interface AppointmentSlotGroup {
247
294
  /** Stable key derived from the actors, so React keys survive a refetch. */
248
295
  readonly key: string;
249
- readonly actors: readonly SchedulingActor[];
296
+ readonly actors: readonly SchedulingActorValue[];
250
297
  readonly durationMinutes: number;
251
298
  /** Sorted by start time. */
252
299
  readonly appointments: readonly Appointment[];
@@ -274,9 +321,11 @@ export declare interface AppointmentSlotGroupCardProps {
274
321
  * Actor types whose schedules may be offered for booking, in the order they are
275
322
  * asked about.
276
323
  *
277
- * Each type becomes one question, because `$find` intersects the schedules it is
278
- * given: a time is offered only when the practitioner *and* the location *and*
279
- * the device are all free for it.
324
+ * Each type becomes its own question, because `$find` intersects the schedules it
325
+ * is given: a time is offered only when the practitioner *and* the location *and*
326
+ * the device are all free for it. Asking for *either* of two practitioners is a
327
+ * second request rather than a second schedule on the same one — see
328
+ * `getActorCombinations`.
280
329
  *
281
330
  * Does not include `PractitionerRole` to prevent double-booking a `Practitioner`
282
331
  * who holds multiple roles. A practitioner is booked on their `Practitioner`,
@@ -325,6 +374,26 @@ declare interface CommonProps {
325
374
  readonly onCancel?: () => void;
326
375
  }
327
376
 
377
+ /**
378
+ * Counts the combinations the selections expand into, without building them.
379
+ *
380
+ * An upper bound rather than an exact count: the ones that would name the same
381
+ * actor twice are only recognised while building. Enough to decide whether the
382
+ * product is worth expanding at all.
383
+ *
384
+ * @param selections - What has been chosen.
385
+ * @returns How many ways there are of satisfying every row, or 0 when nothing
386
+ * has been chosen.
387
+ */
388
+ export declare function countActorCombinations(selections: ActorSelections): number;
389
+
390
+ /**
391
+ * Opens a new requirement row.
392
+ * @param candidates - What it starts out asking for. Empty by default.
393
+ * @returns The row.
394
+ */
395
+ export declare function createActorRequirement(candidates?: readonly ScheduleCandidate[]): ActorRequirement;
396
+
328
397
  /** The days a search covers. Either end may be open. */
329
398
  export declare interface DateRange {
330
399
  readonly start?: Date;
@@ -426,11 +495,17 @@ export declare interface FormatZonedTimeOptions {
426
495
  * Builds the sets of actors an appointment could be held on.
427
496
  *
428
497
  * One combination is one `$find` request: the schedules within it are
429
- * intersected, so its times are the times all of those actors are free.
498
+ * intersected, so its times are the times all of those actors are free. Rows are
499
+ * ANDed and the alternatives within a row are ORed, so the combinations are the
500
+ * product of the rows — one alternative taken from each.
501
+ *
502
+ * Ordered as an odometer with the last row turning fastest, so the first
503
+ * combination is every row's first pick and the rounds the search runs stay
504
+ * predictable.
430
505
  *
431
506
  * @param selections - What has been chosen.
432
- * @returns One combination holding every chosen actor, in role order, or an
433
- * empty list when nothing is chosen.
507
+ * @returns One combination per way of satisfying every row, or an empty list
508
+ * when nothing is chosen.
434
509
  */
435
510
  export declare function getActorCombinations(selections: ActorSelections): ActorCombination[];
436
511
 
@@ -449,16 +524,16 @@ export declare function getActorGroupKey(appointment: Appointment): string;
449
524
  export declare function getActorsKey(actors: readonly Reference[]): string;
450
525
 
451
526
  /**
452
- * Extracts the resource type from a reference to an actor.
527
+ * Extracts the resource type of an actor, given as itself or as a reference.
453
528
  *
454
529
  * Only supports references that may be used for Scheduling operations, which are
455
530
  * those that have a qualified `reference` attribute. The attribute must be present,
456
531
  * and may not be a reference to a "contained" resource (eg. `{ reference: "#cid" }`)
457
532
  *
458
- * @param reference - A Reference to a schedulable resource
459
- * @returns The type that the reference refers to.
533
+ * @param actor - A schedulable resource, or a Reference to one
534
+ * @returns The type of the actor.
460
535
  */
461
- export declare function getActorType(reference: SchedulingActor): SchedulingActorType;
536
+ export declare function getActorType(actor: SchedulingActorValue): SchedulingActorType;
462
537
 
463
538
  /**
464
539
  * Provides the label we use for scheduling resource types in this UI
@@ -468,6 +543,14 @@ export declare function getActorType(reference: SchedulingActor): SchedulingActo
468
543
  */
469
544
  export declare function getActorTypeLabel(resourceType: SchedulingActorType): string;
470
545
 
546
+ /**
547
+ * Get the actor(s) of an appointment.
548
+ * @param appointment - The proposed appointment.
549
+ * @param actorResources - Map of actor resources that have already been previously loaded.
550
+ * @returns List of actors. Resource if it's already loaded, reference otherwise.
551
+ */
552
+ export declare function getAppointmentActors(appointment: Appointment | undefined, actorResources?: ReadonlyMap<string, SchedulingActorResource>): SchedulingActorValue[];
553
+
471
554
  /**
472
555
  * Returns a key uniquely identifying a proposed appointment.
473
556
  *
@@ -557,18 +640,41 @@ export declare function getFindWindowError(range: DateRange): string | undefined
557
640
  export declare function getNativeInputType(type: 'date' | 'time'): string;
558
641
 
559
642
  /**
560
- * Returns everything chosen, across actor types
643
+ * Returns every row across every actor type, in the order they are asked about.
561
644
  * @param selections - What has been chosen.
562
- * @returns The chosen candidates in `BOOKABLE_ACTOR_TYPES` order
645
+ * @returns The rows, empty ones included.
646
+ */
647
+ export declare function getRequirements(selections: ActorSelections): ActorRequirement[];
648
+
649
+ /**
650
+ * Flattens the loaded actor resources into a map, keyed by their reference.
651
+ * @param selections - What has been chosen.
652
+ * @returns The resource behind each chosen actor the search was able to include.
653
+ */
654
+ export declare function getSelectedActorResources(selections: ActorSelections): Map<string, SchedulingActorResource>;
655
+
656
+ /**
657
+ * Returns everything chosen, across rows and actor types.
658
+ * @param selections - What has been chosen.
659
+ * @returns The chosen candidates in `BOOKABLE_ACTOR_TYPES` order, then row order.
563
660
  */
564
661
  export declare function getSelectedCandidates(selections: ActorSelections): ScheduleCandidate[];
565
662
 
566
663
  /**
567
664
  * Reports why the current selections cannot be searched, if they cannot.
568
665
  * @param selections - What has been chosen.
569
- * @returns A message to show the user, or undefined when the search can run.
666
+ * @returns The blocker to show the user, or undefined when the search can run.
667
+ */
668
+ export declare function getSelectionError(selections: ActorSelections): SelectionBlocker | undefined;
669
+
670
+ /**
671
+ * Finds the rows that cannot all be filled at once, when there are any. Specifically happens
672
+ * when two rows offer the same resource, so one resource cannot fill both halves of a visit.
673
+ * @param selections - What has been chosen.
674
+ * @returns The first type whose rows have no answer between them, or undefined when
675
+ * every type can be satisfied.
570
676
  */
571
- export declare function getSelectionError(selections: ActorSelections): string | undefined;
677
+ export declare function getUnsatisfiableRows(selections: ActorSelections): UnsatisfiableRows | undefined;
572
678
 
573
679
  /**
574
680
  * Search window for a calendar day in the given timezone, starting from now if that day is already underway.
@@ -587,9 +693,11 @@ export declare function getZonedDayRange(day: Date, timezone?: string): Required
587
693
  * @param searched - Days to list whether or not they offer anything, so a searched day
588
694
  * that came back empty still shows up rather than going missing. Read on the local
589
695
  * calendar, matching how a day is picked.
696
+ * @param actorResources - The actors' own resources, keyed by reference, for
697
+ * whichever of them the caller has already read. See {@link getAppointmentActors}.
590
698
  * @returns Days in ascending order, each holding its groups.
591
699
  */
592
- export declare function groupAppointmentsByDay(appointments: readonly Appointment[], timezone?: string, searched?: DateRange): AppointmentDay[];
700
+ export declare function groupAppointmentsByDay(appointments: readonly Appointment[], timezone?: string, searched?: DateRange, actorResources?: ReadonlyMap<string, SchedulingActorResource>): AppointmentDay[];
593
701
 
594
702
  /**
595
703
  * Whether every requirement a visit type names has been answered.
@@ -624,6 +732,15 @@ export declare function isRequirementAnswered(requirement: SchedulingRequirement
624
732
  */
625
733
  export declare function isViewerTimezone(timezone: string | undefined, viewer?: string): boolean;
626
734
 
735
+ /**
736
+ * The most alternatives worth expanding into requests at all.
737
+ *
738
+ * The search runs a few combinations at a time, so this is not the size of a
739
+ * round: it is the point past which the whole product is more than anyone is
740
+ * going to sit through, whatever order it is asked in.
741
+ */
742
+ export declare const MAX_ACTOR_COMBINATIONS = 100;
743
+
627
744
  /**
628
745
  * The longest window `Appointment/$find` accepts. Requests wider than this are
629
746
  * rejected outright, so callers have to keep their own date range inside it.
@@ -713,7 +830,7 @@ export declare type ScheduleAvailabilityEditorProps = ScheduleOverrideEditorProp
713
830
  export declare interface ScheduleCandidate {
714
831
  readonly schedule: WithId<Schedule>;
715
832
  /** The actor itself, when the search was able to include it. */
716
- readonly actorResource: WithId<Resource> | undefined;
833
+ readonly actorResource: SchedulingActorResource | undefined;
717
834
  }
718
835
 
719
836
  /**
@@ -734,7 +851,15 @@ export declare interface ScheduleOverrideEditorProps extends CommonProps {
734
851
  */
735
852
  export declare type SchedulingActor = Schedule['actor'][number];
736
853
 
737
- export declare type SchedulingActorType = Dereference<SchedulingActor>['resourceType'];
854
+ /** A loaded actor resource. */
855
+ export declare type SchedulingActorResource = WithId<Dereference<SchedulingActor>>;
856
+
857
+ export declare type SchedulingActorType = SchedulingActorResource['resourceType'];
858
+
859
+ /**
860
+ * An actor as a reference, or as the resource when it has already been read.
861
+ */
862
+ export declare type SchedulingActorValue = SchedulingActor | SchedulingActorResource;
738
863
 
739
864
  /**
740
865
  * A data-coordination component pairing {@link CalendarsPanel} with {@link MultiCalendar}.
@@ -801,6 +926,20 @@ export declare interface SearchScheduleCandidatesOptions {
801
926
  readonly count?: number;
802
927
  }
803
928
 
929
+ /** Why the current selections cannot be searched. */
930
+ export declare interface SelectionBlocker {
931
+ /** A whole sentence to show the user. */
932
+ readonly message: string;
933
+ readonly severity: SelectionBlockerSeverity;
934
+ }
935
+
936
+ /**
937
+ * Whether a blocker is a form that is not finished yet, or answers that cannot work.
938
+ *
939
+ * Only `invalid` is the user's to undo, so only it is worth showing as an error.
940
+ */
941
+ export declare type SelectionBlockerSeverity = 'incomplete' | 'invalid';
942
+
804
943
  /**
805
944
  * Props for editing a service's own default hours, in place of any one calendar's override.
806
945
  * @param schedule - Omitted, which is what selects this mode.
@@ -852,6 +991,14 @@ export declare type TimeOfDay = 'any' | 'morning' | 'afternoon';
852
991
  */
853
992
  export declare function toCodings(elements: readonly ValueSetExpansionContains[]): Coding[];
854
993
 
994
+ /** Rows that cannot all be filled at once, and where to say so. */
995
+ export declare interface UnsatisfiableRows {
996
+ /** The actor type whose rows have no answer between them. */
997
+ readonly actorType: BookableActorType;
998
+ /** What to do about it, to show against those rows. */
999
+ readonly message: string;
1000
+ }
1001
+
855
1002
  /**
856
1003
  * Searches for the times an appointment could be held at.
857
1004
  *