@medplum/react-scheduling 5.1.39 → 5.1.40

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.
@@ -1,6 +1,8 @@
1
1
  import type { Appointment } from '@medplum/fhirtypes';
2
+ import type { Bundle } from '@medplum/fhirtypes';
2
3
  import type { Coding } from '@medplum/fhirtypes';
3
4
  import type { Dereference } from '@medplum/core';
5
+ import type { Extension } from '@medplum/fhirtypes';
4
6
  import type { HealthcareService } from '@medplum/fhirtypes';
5
7
  import type { HealthcareServiceAvailableTime } from '@medplum/fhirtypes';
6
8
  import type { JSX } from 'react';
@@ -122,16 +124,12 @@ export declare interface AppointmentActorSelectProps {
122
124
  }
123
125
 
124
126
  /** What a booking wrote, as `Appointment/$book` returned it. */
125
- export declare interface AppointmentBooking {
126
- readonly appointment: WithId<Appointment>;
127
- /** The times reserved for it, one per schedule it is held on. */
128
- readonly slots: readonly WithId<Slot>[];
129
- }
127
+ export declare type AppointmentBooking = AppointmentWrite;
130
128
 
131
129
  /**
132
130
  * The booking form, writing the booking itself.
133
131
  *
134
- * Wraps {@link AppointmentProposalForm}: posts `Appointment/$book`, announces the
132
+ * Wraps {@link AppointmentProposalForm}: writes the booking, announces the
135
133
  * appointment and every time it reserved so views reading them refresh, then
136
134
  * reports what was written through `onBooked` — the only required prop.
137
135
  *
@@ -142,7 +140,7 @@ export declare interface AppointmentBooking {
142
140
  */
143
141
  export declare function AppointmentBookingForm(props: AppointmentBookingFormProps): JSX.Element;
144
142
 
145
- export declare interface AppointmentBookingFormProps extends Omit<AppointmentProposalFormProps, 'onBook'> {
143
+ export declare interface AppointmentBookingFormProps extends Omit<AppointmentProposalFormProps, 'onSubmit' | 'mode' | 'ignoreAppointment'> {
146
144
  /**
147
145
  * Called with what the booking wrote.
148
146
  *
@@ -201,12 +199,13 @@ export declare interface AppointmentOptionRowProps {
201
199
 
202
200
  /**
203
201
  * Gathers what a visit is held on, finds a time every one of them is free, and
204
- * hands the proposal out to be booked.
202
+ * hands the proposal out to be written.
205
203
  *
206
- * Writes nothing and announces nothing: `onBook` owns that. Mount this to do
204
+ * Writes nothing and announces nothing: `onSubmit` owns that. Mount this to do
207
205
  * something other than `$book` with the proposal — hold it through `$hold`, or
208
206
  * write it inside a transaction of your own. {@link AppointmentBookingForm} is the
209
- * one that books.
207
+ * one that books, and {@link AppointmentRescheduleForm} the one that moves a visit
208
+ * already on file.
210
209
  *
211
210
  * @param props - The React props.
212
211
  * @returns The form.
@@ -214,6 +213,8 @@ export declare interface AppointmentOptionRowProps {
214
213
  export declare function AppointmentProposalForm(props: AppointmentProposalFormProps): JSX.Element;
215
214
 
216
215
  export declare interface AppointmentProposalFormProps {
216
+ /** What the proposal is for. Defaults to booking a new visit. */
217
+ readonly mode?: AppointmentProposalMode;
217
218
  /** Pre-fills where the visit is, for a host that already knows. */
218
219
  readonly defaultLocation?: WithId<Location_2>;
219
220
  /** Pre-fills the visit type, for a deep link or a reschedule. */
@@ -221,9 +222,23 @@ export declare interface AppointmentProposalFormProps {
221
222
  /** Pre-fills who the visit is for, for a host launching from a patient's chart. */
222
223
  readonly defaultPatient?: WithId<Patient>;
223
224
  /**
224
- * The day the time search opens on. Defaults to today.
225
+ * Pre-fills who and what the visit is held on, for a host that already knows — the
226
+ * actors an appointment being moved is currently held on, say.
227
+ *
228
+ * Read once, at mount: the fields own what they are asked for afterwards.
229
+ */
230
+ readonly defaultSelections?: ActorSelections;
231
+ /**
232
+ * An appointment whose own times are not to count as taken.
225
233
  *
226
- * A day, not a time: only a time `$find` offered can be chosen.
234
+ * For a form searching on behalf of an appointment that already exists: without it,
235
+ * the time it holds blocks every search that keeps any of the actors holding it, and
236
+ * moving a visit to a different room at the same hour finds nothing.
237
+ */
238
+ readonly ignoreAppointment?: Reference<Appointment> | WithId<Appointment>;
239
+ /**
240
+ * The day the time search opens on, and the day a typed time starts out on.
241
+ * Defaults to today.
227
242
  */
228
243
  readonly defaultStart?: Date;
229
244
  /**
@@ -256,12 +271,78 @@ export declare interface AppointmentProposalFormProps {
256
271
  /** The ValueSet the diagnosis code field binds to. Defaults to the full ICD-10-CM value set. */
257
272
  readonly diagnosisBinding?: string;
258
273
  /**
259
- * Performs the booking with the proposal the form assembled.
274
+ * Writes the proposal the form assembled.
260
275
  *
261
- * Resolving marks the form booked, so it stops offering to book until an answer
262
- * changes; rejecting shows the reason as the booking's refusal, every answer kept.
276
+ * Resolving marks the form written, so it stops offering to write until an answer
277
+ * changes; rejecting shows the reason as the write's refusal, every answer kept.
278
+ *
279
+ * In `book` mode the proposal carries the patient and the visit type's required
280
+ * codes; in `reschedule` mode it is the time as `$find` offered it, untouched.
281
+ */
282
+ readonly onSubmit: (proposal: Appointment, options: BookOptions) => void | Promise<void>;
283
+ /** Extensions to put on every appointment this form books. */
284
+ readonly appointmentExtensions?: readonly Extension[];
285
+ /**
286
+ * Allows for manual entry of a time and length, bypassing the `$find` search and
287
+ * `$book` endpoint.
263
288
  */
264
- readonly onBook: (proposal: Appointment) => void | Promise<void>;
289
+ readonly canBypassSchedulingRules?: boolean;
290
+ }
291
+
292
+ /**
293
+ * What the proposal the form assembles is for, which decides what it asks for.
294
+ *
295
+ * `book` gathers everything a new visit is written with: who it is for, and whatever
296
+ * its visit type requires. `reschedule` gathers only what decides the time, because
297
+ * `Appointment/[id]/$reschedule` moves a visit already on file and writes nothing else
298
+ * about it — asking for details it would drop is asking for them under false pretences.
299
+ *
300
+ * The visit type goes the same way: in `reschedule` mode a `defaultService` is shown
301
+ * rather than asked for, since the operation is measured against it but will not change
302
+ * it. Without one it is still asked, because the search cannot run without a visit type.
303
+ */
304
+ export declare type AppointmentProposalMode = 'book' | 'reschedule';
305
+
306
+ /** What a reschedule wrote, as `Appointment/[id]/$reschedule` returned it. */
307
+ export declare type AppointmentReschedule = AppointmentWrite;
308
+
309
+ /**
310
+ * The form that moves a visit already on file, to another time or another set of actors.
311
+ *
312
+ * Wraps {@link AppointmentProposalForm}, opened on what the appointment is held on now
313
+ * and searching as though it were not: the time it occupies is the time it is being
314
+ * moved off, so `$find` is told to ignore it, which is what lets the same hour in a
315
+ * different room be found.
316
+ *
317
+ * Posts `Appointment/[id]/$reschedule`, announces the appointment, the times it gave up
318
+ * and the times it took, so views reading them refresh, then reports what was written
319
+ * through `onRescheduled`.
320
+ *
321
+ * Only the time and the actors are asked for. Everything else on the appointment — who
322
+ * it is for, its status, its visit type, whatever clinical detail it carries — is left
323
+ * exactly as it was, because that is all the operation will write. The visit type is
324
+ * shown rather than offered: `$reschedule` takes none, reading the one the appointment
325
+ * is on file for instead, since changing what a visit *is* would leave its required
326
+ * codes, its authorization, and anything applied at booking keyed to a type it no
327
+ * longer has.
328
+ *
329
+ * @param props - The React props.
330
+ * @returns The form.
331
+ */
332
+ export declare function AppointmentRescheduleForm(props: AppointmentRescheduleFormProps): JSX.Element;
333
+
334
+ export declare interface AppointmentRescheduleFormProps extends Omit<AppointmentProposalFormProps, 'onSubmit' | 'mode' | 'defaultService' | 'defaultSelections' | 'defaultPatient' | 'ignoreAppointment' | 'mrnSystem' | 'procedureBinding' | 'diagnosisBinding' | 'canBypassSchedulingRules' | 'appointmentExtensions'> {
335
+ /** The appointment being moved. */
336
+ readonly appointment: WithId<Appointment>;
337
+ /**
338
+ * Called with what the move wrote.
339
+ *
340
+ * The answers stay on screen and the form stops offering to move until one of them
341
+ * changes, so a host can keep this mounted without it writing twice. A failure here
342
+ * is logged rather than shown as a refusal: the appointment has already moved by the
343
+ * time it runs.
344
+ */
345
+ readonly onRescheduled: (reschedule: AppointmentReschedule) => void | Promise<void>;
265
346
  }
266
347
 
267
348
  /**
@@ -285,6 +366,8 @@ export declare interface AppointmentServiceSelectProps {
285
366
  /** A chosen site, which narrows the services on offer to the ones held there. */
286
367
  readonly location?: WithId<Location_2> | Reference<Location_2>;
287
368
  readonly label?: string;
369
+ readonly placeholder?: string;
370
+ readonly required?: boolean;
288
371
  readonly error?: string;
289
372
  readonly disabled?: boolean;
290
373
  }
@@ -317,6 +400,13 @@ export declare interface AppointmentSlotGroupCardProps {
317
400
  readonly disabled?: boolean;
318
401
  }
319
402
 
403
+ /** What a scheduling operation wrote: the appointment, and the times it holds. */
404
+ export declare interface AppointmentWrite {
405
+ readonly appointment: WithId<Appointment>;
406
+ /** The times reserved for it, one set per schedule it is held on. */
407
+ readonly slots: readonly WithId<Slot>[];
408
+ }
409
+
320
410
  /**
321
411
  * Actor types whose schedules may be offered for booking, in the order they are
322
412
  * asked about.
@@ -346,6 +436,12 @@ export declare interface BookingRequirementValues {
346
436
  readonly medicalNecessity: boolean;
347
437
  }
348
438
 
439
+ /** What `onBook` is told about the proposal it was handed. */
440
+ export declare interface BookOptions {
441
+ /** True when the time was typed (& unvalidated) rather than chosen from the search. */
442
+ readonly manual: boolean;
443
+ }
444
+
349
445
  export declare function Calendar(props: CalendarProps): JSX.Element;
350
446
 
351
447
  export declare interface CalendarProps {
@@ -630,14 +726,14 @@ export declare function getFindWindowError(range: DateRange): string | undefined
630
726
  /**
631
727
  * Returns the input type to use for a date or time field.
632
728
  *
633
- * JSDOM does not fire change events for `<input type="date">` or
634
- * `<input type="time">`, so tests get a plain text field, matching what
729
+ * JSDOM does not fire change events for `<input type="date">`, `<input type="time">`
730
+ * or `<input type="datetime-local">`, so tests get a plain text field, matching what
635
731
  * `DateTimeInput` does.
636
732
  *
637
733
  * @param type - The native input type to use outside of tests.
638
734
  * @returns The input type for the current environment.
639
735
  */
640
- export declare function getNativeInputType(type: 'date' | 'time'): string;
736
+ export declare function getNativeInputType(type: 'date' | 'time' | 'datetime-local'): string;
641
737
 
642
738
  /**
643
739
  * Returns every row across every actor type, in the order they are asked about.
@@ -787,6 +883,14 @@ export declare interface MultiCalendarSource {
787
883
  */
788
884
  export declare function parseDayKey(key: string): Date;
789
885
 
886
+ /**
887
+ * Reads a native datetime input's value as an instant in a timezone.
888
+ * @param value - A `YYYY-MM-DDTHH:MM` value, as the input reports it.
889
+ * @param timezone - IANA timezone identifier. Defaults to the browser's.
890
+ * @returns The instant, or undefined while the value is not a complete date and time.
891
+ */
892
+ export declare function parseZonedDateTimeInput(value: string, timezone?: string): Date | undefined;
893
+
790
894
  /**
791
895
  * Reads a wall-clock time on a given day as an instant in a timezone.
792
896
  *
@@ -797,6 +901,18 @@ export declare function parseDayKey(key: string): Date;
797
901
  */
798
902
  export declare function parseZonedTime(day: Date, time: string, timezone?: string): Date | undefined;
799
903
 
904
+ /**
905
+ * Reads what a scheduling operation wrote out of the bundle it answers with.
906
+ *
907
+ * `$book` and `$reschedule` both answer with the appointment and every Slot they
908
+ * created, in one transaction bundle.
909
+ *
910
+ * @param written - The bundle the operation returned.
911
+ * @param operation - What was called, for the error a bundle without an appointment raises.
912
+ * @returns The appointment and the times reserved for it.
913
+ */
914
+ export declare function readAppointmentWrite(written: Bundle<WithId<Appointment> | WithId<Slot>>, operation: string): AppointmentWrite;
915
+
800
916
  /**
801
917
  * Actor types that must be chosen before a search can run.
802
918
  *
@@ -805,6 +921,35 @@ export declare function parseZonedTime(day: Date, time: string, timezone?: strin
805
921
  */
806
922
  export declare const REQUIRED_ACTOR_TYPES: ReadonlySet<SchedulingActorType>;
807
923
 
924
+ export declare interface RescheduleDefaults {
925
+ /** The visit type the appointment is on file under, where it records one. */
926
+ readonly service: WithId<HealthcareService> | undefined;
927
+ /** Who and what it is held on now, arranged into the rows a form asks for them in. */
928
+ readonly selections: ActorSelections;
929
+ /**
930
+ * Actors the visit is held on whose schedules cannot be offered back, named where they
931
+ * could be read.
932
+ *
933
+ * A Schedule that is inactive, no longer names the visit type, or is held on an actor
934
+ * this form does not book leaves its actors out of the rows above — and a move writes
935
+ * the actors it is given, so these are about to be dropped off the visit.
936
+ */
937
+ readonly droppedActors: readonly SchedulingActor[];
938
+ /**
939
+ * Set when the visit type, or a Slot or Schedule the visit is held on, could not be read.
940
+ *
941
+ * `$reschedule` reads those same references, so no move from here would be accepted.
942
+ */
943
+ readonly error: unknown;
944
+ /**
945
+ * True until the reads settle.
946
+ *
947
+ * A form reads its defaults once, at mount, so one mounted while this is true would
948
+ * open on nothing and never take them.
949
+ */
950
+ readonly loading: boolean;
951
+ }
952
+
808
953
  /**
809
954
  * Edits weekly availability for one visit service type, either as a Schedule's
810
955
  * override of the service hours or as the service's own default hours.
@@ -871,9 +1016,9 @@ export declare type SchedulingActorValue = SchedulingActor | SchedulingActorReso
871
1016
  * The form writes the booking and announces what it wrote, which is what puts the
872
1017
  * new appointment on the calendar beside it — a host supplies no data for any of it.
873
1018
  * What was written is reported through `onBooked`, for a host that wants to say so.
874
- * - Shows what is booked: clicking an appointment opens {@link AppointmentDetails} in a
875
- * drawer over the calendar, describing the visit and offering to cancel it. Cancelling
876
- * is what takes the time back off the calendar, again without a host supplying anything.
1019
+ * - Shows what is booked: clicking an appointment opens {@link AppointmentDetails} in the
1020
+ * same pane the booking form uses, describing the visit and offering to cancel or
1021
+ * reschedule it.
877
1022
  * - Highlights the time last chosen, wherever it was chosen: the click that opened the
878
1023
  * pane, then whatever the form's time search settles on, and nothing while the form
879
1024
  * holds no time. The calendar is never moved to reach it — a highlight off the week
@@ -892,11 +1037,32 @@ export declare interface SchedulingWorkspaceProps {
892
1037
  readonly diagnosisBinding?: string;
893
1038
  readonly onBooked?: (booking: AppointmentBooking) => void | Promise<void>;
894
1039
  readonly onCancelled?: (appointment: WithId<Appointment>) => void | Promise<void>;
1040
+ readonly onRescheduled?: (reschedule: AppointmentReschedule) => void | Promise<void>;
895
1041
  /**
896
1042
  * Overrides the value set the appointment detail view offers cancellation reasons
897
1043
  * from, for a host coding them against its own terminology.
898
1044
  */
899
1045
  readonly appointmentCancellationReasonValueSet?: string;
1046
+ /**
1047
+ * Lets the booking form take a typed time and length, placing a visit the scheduling
1048
+ * rules would refuse: over occupied or blocked time, past the configured capacity,
1049
+ * or at a time or length the visit type does not offer.
1050
+ *
1051
+ * Passing it draws the fields; it enforces nothing. Which users get it is the host
1052
+ * application's responsibility.
1053
+ *
1054
+ * Such a booking is sent as a transaction, so the appointment and its Slots commit
1055
+ * together on projects with the `transaction-bundles` feature enabled. Without it they
1056
+ * are applied as a plain batch, where an appointment that failed to write would leave
1057
+ * Slots holding no visit.
1058
+ * @see https://www.medplum.com/docs/fhir-datastore/fhir-batch-requests#batches-vs-transactions
1059
+ */
1060
+ readonly canBypassSchedulingRules?: boolean;
1061
+ /**
1062
+ * Extensions to put on every appointment booked from this workspace. See
1063
+ * {@link AppointmentProposalFormProps.appointmentExtensions}.
1064
+ */
1065
+ readonly appointmentExtensions?: readonly Extension[];
900
1066
  }
901
1067
 
902
1068
  /**
@@ -955,10 +1121,10 @@ export declare interface ServiceDefaultEditorProps extends CommonProps {
955
1121
  /**
956
1122
  * Immutably gives a Schedule its own hours for a HealthcareService, in place of the service default.
957
1123
  * Reads back through `getEffectiveAvailability`; to drop the calendar back to the default, clear the
958
- * parameter with `clearScheduleParameter(schedule, service, 'availability')` from `@medplum/core`.
1124
+ * parameter with `clearScheduleSchedulingParameter(schedule, service, 'availability')` from `@medplum/core`.
959
1125
  *
960
1126
  * Availability is the one parameter with a typed wrapper, because it is the only one that is not a single
961
- * `value[x]`: `bufferBefore` and the rest go through `setScheduleParameter` directly, already legible as
1127
+ * `value[x]`: `bufferBefore` and the rest go through `setScheduleSchedulingParameter` directly, already legible as
962
1128
  * `{ url: 'bufferBefore', valueDuration: { value: 10, unit: 'min' } }`. Availability is a repeating nested
963
1129
  * structure, so hand-building it at every call site would mean re-deriving the encoding.
964
1130
  *
@@ -991,6 +1157,8 @@ export declare type TimeOfDay = 'any' | 'morning' | 'afternoon';
991
1157
  */
992
1158
  export declare function toCodings(elements: readonly ValueSetExpansionContains[]): Coding[];
993
1159
 
1160
+ export declare function toScheduleCandidate(schedule: WithId<Schedule>, service: WithId<HealthcareService> | undefined, actors: Map<string, SchedulingActorResource>): ScheduleCandidate | undefined;
1161
+
994
1162
  /** Rows that cannot all be filled at once, and where to say so. */
995
1163
  export declare interface UnsatisfiableRows {
996
1164
  /** The actor type whose rows have no answer between them. */
@@ -1019,6 +1187,14 @@ export declare interface UseProposedAppointmentsOptions {
1019
1187
  readonly range: DateRange;
1020
1188
  /** Times to ask for per combination. Defaults to 20. */
1021
1189
  readonly count?: number;
1190
+ /**
1191
+ * An appointment whose own times are not to count as taken.
1192
+ *
1193
+ * For searching on behalf of an appointment that already exists: the times it holds
1194
+ * are the times it is being moved off, and left standing they would block every
1195
+ * search that keeps any of the actors it is held on.
1196
+ */
1197
+ readonly ignoreAppointment?: Reference<Appointment> | WithId<Appointment>;
1022
1198
  }
1023
1199
 
1024
1200
  export declare interface UseProposedAppointmentsResult {
@@ -1035,6 +1211,23 @@ export declare interface UseProposedAppointmentsResult {
1035
1211
  readonly windowError: string | undefined;
1036
1212
  }
1037
1213
 
1214
+ /**
1215
+ * Reads what an appointment is currently held on, for a form that offers to move it.
1216
+ *
1217
+ * The visit type comes off `Appointment.serviceType`, which carries a reference to the
1218
+ * HealthcareService it was booked under; the actors come off the Slots it holds, which
1219
+ * name the Schedules — the appointment's own participants would not say which of an
1220
+ * actor's schedules is the one being moved off.
1221
+ *
1222
+ * An actor that cannot be read keeps its schedule, shown under the name the schedule
1223
+ * gives it: the move is written against the schedule, not the actor.
1224
+ *
1225
+ * @param appointment - The appointment being moved.
1226
+ * @returns Its visit type and actors, whether they are still being read, who a move
1227
+ * would drop off it, and why it could not be read, where it could not.
1228
+ */
1229
+ export declare function useRescheduleDefaults(appointment: WithId<Appointment>): RescheduleDefaults;
1230
+
1038
1231
  /**
1039
1232
  * Loads the Appointments for a set of schedules within a date range and keeps them live.
1040
1233
  *