@openmrs/esm-appointments-app 9.2.1-pre.7303 → 9.2.1-pre.7315

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.
Files changed (64) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/dist/1431.js +1 -1
  3. package/dist/1431.js.map +1 -1
  4. package/dist/1559.js +1 -1
  5. package/dist/1559.js.map +1 -1
  6. package/dist/2265.js +1 -0
  7. package/dist/2265.js.map +1 -0
  8. package/dist/{5160.js → 4036.js} +1 -1
  9. package/dist/{5160.js.map → 4036.js.map} +1 -1
  10. package/dist/449.js +1 -1
  11. package/dist/449.js.map +1 -1
  12. package/dist/4515.js +1 -0
  13. package/dist/4515.js.map +1 -0
  14. package/dist/4745.js +1 -1
  15. package/dist/4745.js.map +1 -1
  16. package/dist/4889.js +1 -1
  17. package/dist/4889.js.map +1 -1
  18. package/dist/525.js +1 -1
  19. package/dist/525.js.map +1 -1
  20. package/dist/5666.js +1 -1
  21. package/dist/5666.js.map +1 -1
  22. package/dist/5755.js +1 -1
  23. package/dist/5755.js.map +1 -1
  24. package/dist/592.js +1 -1
  25. package/dist/592.js.map +1 -1
  26. package/dist/6467.js +1 -1
  27. package/dist/6467.js.map +1 -1
  28. package/dist/6886.js +1 -1
  29. package/dist/6886.js.map +1 -1
  30. package/dist/{7565.js → 7294.js} +1 -1
  31. package/dist/7294.js.map +1 -0
  32. package/dist/9712.js +1 -1
  33. package/dist/9712.js.map +1 -1
  34. package/dist/main.js +1 -1
  35. package/dist/main.js.map +1 -1
  36. package/dist/openmrs-esm-appointments-app.js.buildmanifest.json +121 -120
  37. package/dist/routes.json +1 -1
  38. package/package.json +1 -1
  39. package/src/appointments/common-components/appointments-table.component.tsx +4 -6
  40. package/src/constants.ts +1 -0
  41. package/src/form/appointments-form.resource.ts +1 -0
  42. package/src/form/appointments-form.test.tsx +111 -71
  43. package/src/form/appointments-form.workspace.tsx +437 -436
  44. package/src/form/exported-appointments-form.workspace.tsx +24 -0
  45. package/src/helpers/functions.ts +72 -25
  46. package/src/index.ts +5 -3
  47. package/src/metrics/metrics-cards/highest-volume-service.extension.tsx +1 -1
  48. package/src/metrics/metrics-cards/metrics-card.component.tsx +1 -1
  49. package/src/metrics/metrics-header.component.tsx +9 -24
  50. package/src/patient-appointments/patient-appointments-action-menu.component.tsx +2 -6
  51. package/src/patient-appointments/patient-appointments-detailed-summary.extension.tsx +176 -15
  52. package/src/patient-appointments/{patient-appointments-base.test.tsx → patient-appointments-detailed-summary.test.tsx} +14 -22
  53. package/src/patient-appointments/patient-appointments-overview.component.tsx +15 -18
  54. package/src/routes.json +22 -7
  55. package/dist/3092.js +0 -1
  56. package/dist/3092.js.map +0 -1
  57. package/dist/7026.js +0 -1
  58. package/dist/7026.js.map +0 -1
  59. package/dist/7565.js.map +0 -1
  60. package/src/hooks/patient-appointment-context.ts +0 -18
  61. package/src/patient-appointments/patient-appointments-base.component.tsx +0 -178
  62. package/src/patient-search/patient-search.component.tsx +0 -33
  63. package/src/patient-search/patient-search.scss +0 -24
  64. /package/src/patient-appointments/{patient-appointments-base.scss → patient-appointments-detailed-summary.scss} +0 -0
@@ -33,7 +33,8 @@ import {
33
33
  useLocations,
34
34
  usePatient,
35
35
  useSession,
36
- type DefaultWorkspaceProps,
36
+ Workspace2,
37
+ type Workspace2DefinitionProps,
37
38
  type FetchResponse,
38
39
  } from '@openmrs/esm-framework';
39
40
  import { z } from 'zod';
@@ -55,21 +56,20 @@ import styles from './appointments-form.scss';
55
56
  interface AppointmentsFormProps {
56
57
  appointment?: Appointment;
57
58
  recurringPattern?: RecurringPattern;
58
- patientUuid?: string;
59
- context: string;
59
+ patientUuid: string;
60
60
  }
61
61
 
62
62
  const time12HourFormatRegexPattern = '^(1[0-2]|0?[1-9]):[0-5][0-9]$';
63
+ const time12HourFormatRegex = /^(1[0-2]|0?[1-9]):[0-5][0-9]$/;
63
64
 
64
- const isValidTime = (timeStr: string) => timeStr.match(new RegExp(time12HourFormatRegexPattern));
65
+ const isValidTime = (timeStr: string) => time12HourFormatRegex.test(timeStr);
65
66
 
66
- const AppointmentsForm: React.FC<AppointmentsFormProps & DefaultWorkspaceProps> = ({
67
- appointment,
68
- recurringPattern,
69
- patientUuid,
70
- context,
67
+ /**
68
+ * Workspace used to create or edit an appointment within the appointments app
69
+ */
70
+ const AppointmentsForm: React.FC<Workspace2DefinitionProps<AppointmentsFormProps>> = ({
71
+ workspaceProps: { appointment, recurringPattern, patientUuid },
71
72
  closeWorkspace,
72
- promptBeforeClosing,
73
73
  }) => {
74
74
  const { patient } = usePatient(patientUuid);
75
75
  const { mutateAppointments } = useMutateAppointments();
@@ -264,7 +264,7 @@ const AppointmentsForm: React.FC<AppointmentsFormProps & DefaultWorkspaceProps>
264
264
 
265
265
  useEffect(() => setValue('formIsRecurringAppointment', isRecurringAppointment), [isRecurringAppointment, setValue]);
266
266
 
267
- // Retrive ref callback for appointmentDateTime (startDate & recurringPatternEndDate)
267
+ // Retrieve ref callback for appointmentDateTime (startDate & recurringPatternEndDate)
268
268
  const {
269
269
  field: { ref: startDateRef },
270
270
  } = useController({ name: 'appointmentDateTime.startDate', control });
@@ -283,12 +283,9 @@ const AppointmentsForm: React.FC<AppointmentsFormProps & DefaultWorkspaceProps>
283
283
  useEffect(() => {
284
284
  if (isSuccessful) {
285
285
  reset();
286
- promptBeforeClosing(() => false);
287
- closeWorkspace();
288
- return;
286
+ closeWorkspace({ discardUnsavedChanges: true, closeWindow: true });
289
287
  }
290
- promptBeforeClosing(() => isDirty);
291
- }, [isDirty, promptBeforeClosing, isSuccessful, reset, closeWorkspace]);
288
+ }, [isSuccessful, reset, closeWorkspace]);
292
289
 
293
290
  const handleWorkloadDateChange = (date: Date) => {
294
291
  const appointmentDate = getValues('appointmentDateTime');
@@ -331,23 +328,21 @@ const AppointmentsForm: React.FC<AppointmentsFormProps & DefaultWorkspaceProps>
331
328
  }
332
329
  })();
333
330
 
331
+ const isEditing = Boolean(appointment);
332
+
334
333
  // Same for creating and editing
335
334
  const handleSaveAppointment = async (data: AppointmentFormData) => {
336
335
  setIsSubmitting(true);
337
336
  // Construct appointment payload
338
337
  const appointmentPayload = constructAppointmentPayload(data);
339
338
 
340
- // check if Duplicate Response Occurs
339
+ // Check if a duplicate response occurs
341
340
  const response: FetchResponse = await checkAppointmentConflict(appointmentPayload);
342
341
  let errorMessage = t('appointmentConflict', 'Appointment conflict');
343
342
  if (response?.data?.hasOwnProperty('SERVICE_UNAVAILABLE')) {
344
343
  errorMessage = t('serviceUnavailable', 'Appointment time is outside of service hours');
345
344
  } else if (response?.data?.hasOwnProperty('PATIENT_DOUBLE_BOOKING')) {
346
- if (context !== 'editing') {
347
- errorMessage = t('patientDoubleBooking', 'Patient already booked for an appointment at this time');
348
- } else {
349
- errorMessage = null;
350
- }
345
+ errorMessage = t('patientDoubleBooking', 'Patient already booked for an appointment at this time');
351
346
  }
352
347
 
353
348
  if (response.status === 200 && errorMessage) {
@@ -381,19 +376,17 @@ const AppointmentsForm: React.FC<AppointmentsFormProps & DefaultWorkspaceProps>
381
376
  isLowContrast: true,
382
377
  kind: 'success',
383
378
  subtitle: t('appointmentNowVisible', 'It is now visible on the Appointments page'),
384
- title:
385
- context === 'editing'
386
- ? t('appointmentEdited', 'Appointment edited')
387
- : t('appointmentScheduled', 'Appointment scheduled'),
379
+ title: isEditing
380
+ ? t('appointmentEdited', 'Appointment edited')
381
+ : t('appointmentScheduled', 'Appointment scheduled'),
388
382
  });
389
383
  }
390
384
  if (status === 204) {
391
385
  setIsSubmitting(false);
392
386
  showSnackbar({
393
- title:
394
- context === 'editing'
395
- ? t('appointmentEditError', 'Error editing appointment')
396
- : t('appointmentFormError', 'Error scheduling appointment'),
387
+ title: isEditing
388
+ ? t('appointmentEditError', 'Error editing appointment')
389
+ : t('appointmentFormError', 'Error scheduling appointment'),
397
390
  kind: 'error',
398
391
  isLowContrast: false,
399
392
  subtitle: t('noContent', 'No Content'),
@@ -403,10 +396,9 @@ const AppointmentsForm: React.FC<AppointmentsFormProps & DefaultWorkspaceProps>
403
396
  (error) => {
404
397
  setIsSubmitting(false);
405
398
  showSnackbar({
406
- title:
407
- context === 'editing'
408
- ? t('appointmentEditError', 'Error editing appointment')
409
- : t('appointmentFormError', 'Error scheduling appointment'),
399
+ title: isEditing
400
+ ? t('appointmentEditError', 'Error editing appointment')
401
+ : t('appointmentFormError', 'Error scheduling appointment'),
410
402
  kind: 'error',
411
403
  isLowContrast: false,
412
404
  subtitle: error?.message,
@@ -450,7 +442,7 @@ const AppointmentsForm: React.FC<AppointmentsFormProps & DefaultWorkspaceProps>
450
442
  providers: [{ uuid: provider }],
451
443
  patientUuid: patientUuid,
452
444
  comments: appointmentNote,
453
- uuid: context === 'editing' ? appointment.uuid : undefined,
445
+ uuid: isEditing ? appointment.uuid : undefined,
454
446
  dateAppointmentScheduled: dayjs(dateAppointmentScheduled).format(),
455
447
  };
456
448
  };
@@ -474,346 +466,380 @@ const AppointmentsForm: React.FC<AppointmentsFormProps & DefaultWorkspaceProps>
474
466
  };
475
467
  };
476
468
 
477
- if (isLoading)
469
+ if (isLoading) {
478
470
  return (
479
471
  <InlineLoading className={styles.loader} description={`${t('loading', 'Loading')} ...`} role="progressbar" />
480
472
  );
473
+ }
474
+
475
+ const title = isEditing
476
+ ? t('editAppointment', 'Edit appointment')
477
+ : t('createNewAppointment', 'Create new appointment');
481
478
 
482
479
  return (
483
- <Form onSubmit={handleSubmit(handleSaveAppointment)}>
484
- {patient && (
485
- <ExtensionSlot
486
- name="patient-header-slot"
487
- state={{
488
- patient,
489
- patientUuid: patientUuid,
490
- hideActionsOverflow: true,
491
- }}
492
- />
493
- )}
494
- <Stack className={styles.formWrapper} gap={6}>
495
- <FormGroup className={styles.formGroup} legendText={t('location', 'Location')}>
496
- <ResponsiveWrapper>
497
- <Controller
498
- name="location"
499
- control={control}
500
- render={({ field: { onChange, value, onBlur, ref } }) => (
501
- <Select
502
- id="location"
503
- invalid={!!errors?.location}
504
- invalidText={errors?.location?.message}
505
- labelText={t('selectALocation', 'Select a location')}
506
- onChange={onChange}
507
- onBlur={onBlur}
508
- ref={ref}
509
- value={value}>
510
- <SelectItem text={t('chooseLocation', 'Choose a location')} value="" />
511
- {locations?.length > 0 &&
512
- locations.map((location) => (
513
- <SelectItem key={location.uuid} text={location.display} value={location.uuid}>
514
- {location.display}
515
- </SelectItem>
516
- ))}
517
- </Select>
518
- )}
519
- />
520
- </ResponsiveWrapper>
521
- </FormGroup>
522
- <FormGroup className={styles.formGroup} legendText={t('service', 'Service')}>
523
- <ResponsiveWrapper>
524
- <Controller
525
- name="selectedService"
526
- control={control}
527
- render={({ field: { onBlur, onChange, value, ref } }) => (
528
- <Select
529
- id="service"
530
- invalid={!!errors?.selectedService}
531
- invalidText={errors?.selectedService?.message}
532
- labelText={t('selectService', 'Select a service')}
533
- onBlur={onBlur}
534
- onChange={(event) => {
535
- if (context === 'creating') {
536
- setValue(
537
- 'duration',
538
- services?.find((service) => service.name === event.target.value)?.durationMins,
539
- );
540
- } else if (context === 'editing') {
541
- const previousServiceDuration = services?.find(
542
- (service) => service.name === getValues('selectedService'),
543
- )?.durationMins;
544
- const selectedServiceDuration = services?.find(
545
- (service) => service.name === event.target.value,
546
- )?.durationMins;
547
- if (selectedServiceDuration && previousServiceDuration === getValues('duration')) {
548
- setValue('duration', selectedServiceDuration);
549
- }
550
- }
551
- onChange(event);
552
- }}
553
- ref={ref}
554
- value={value}>
555
- <SelectItem text={t('chooseService', 'Select service')} value="" />
556
- {services?.length > 0 &&
557
- services.map((service) => (
558
- <SelectItem key={service.uuid} text={service.name} value={service.name}>
559
- {service.name}
560
- </SelectItem>
561
- ))}
562
- </Select>
563
- )}
564
- />
565
- </ResponsiveWrapper>
566
- </FormGroup>
567
- <FormGroup className={styles.formGroup} legendText={t('appointmentType_title', 'Appointment Type')}>
568
- <ResponsiveWrapper>
569
- <Controller
570
- name="appointmentType"
571
- control={control}
572
- render={({ field: { onBlur, onChange, value, ref } }) => (
573
- <Select
574
- disabled={!appointmentTypes?.length}
575
- id="appointmentType"
576
- invalid={!!errors?.appointmentType}
577
- invalidText={errors?.appointmentType?.message}
578
- labelText={t('selectAppointmentType', 'Select the type of appointment')}
579
- onBlur={onBlur}
580
- onChange={onChange}
581
- ref={ref}
582
- value={value}>
583
- <SelectItem text={t('chooseAppointmentType', 'Choose appointment type')} value="" />
584
- {appointmentTypes?.length > 0 &&
585
- appointmentTypes.map((appointmentType, index) => (
586
- <SelectItem key={index} text={appointmentType} value={appointmentType}>
587
- {appointmentType}
588
- </SelectItem>
589
- ))}
590
- </Select>
591
- )}
592
- />
593
- </ResponsiveWrapper>
594
- </FormGroup>
595
-
596
- <FormGroup className={styles.formGroup} legendText={t('recurringAppointment', 'Recurring Appointment')}>
597
- <div>
598
- <Toggle
599
- id="recurringToggle"
600
- labelB={t('yes', 'Yes')}
601
- labelA={t('no', 'No')}
602
- labelText={t('isRecurringAppointment', 'Is this a recurring appointment?')}
603
- onClick={() => setIsRecurringAppointment(!isRecurringAppointment)}
604
- />
605
- </div>
606
- </FormGroup>
607
-
608
- <FormGroup className={styles.formGroup} legendText={t('dateTime', 'Date & Time')}>
609
- <div className={styles.dateTimeFields}>
610
- {isRecurringAppointment && (
611
- <div className={styles.inputContainer}>
612
- {allowAllDayAppointments && (
613
- <Controller
614
- name="isAllDayAppointment"
615
- control={control}
616
- render={({ field: { value, onChange } }) => (
617
- <Toggle
618
- id="allDayToggle"
619
- labelA={t('no', 'No')}
620
- labelB={t('yes', 'Yes')}
621
- labelText={t('allDay', 'All day')}
622
- toggled={value}
623
- onToggle={onChange}
624
- />
625
- )}
626
- />
480
+ <Workspace2 title={title} hasUnsavedChanges={isDirty}>
481
+ <Form onSubmit={handleSubmit(handleSaveAppointment)}>
482
+ {patient && (
483
+ <ExtensionSlot
484
+ name="patient-header-slot"
485
+ state={{
486
+ patient,
487
+ patientUuid: patientUuid,
488
+ hideActionsOverflow: true,
489
+ }}
490
+ />
491
+ )}
492
+ <Stack className={styles.formWrapper} gap={6}>
493
+ <FormGroup className={styles.formGroup} legendText={t('location', 'Location')}>
494
+ <ResponsiveWrapper>
495
+ <Controller
496
+ name="location"
497
+ control={control}
498
+ render={({ field: { onChange, value, onBlur, ref } }) => (
499
+ <Select
500
+ id="location"
501
+ invalid={!!errors?.location}
502
+ invalidText={errors?.location?.message}
503
+ labelText={t('selectALocation', 'Select a location')}
504
+ onChange={onChange}
505
+ onBlur={onBlur}
506
+ ref={ref}
507
+ value={value}>
508
+ <SelectItem text={t('chooseLocation', 'Choose a location')} value="" />
509
+ {locations?.length > 0 &&
510
+ locations.map((location) => (
511
+ <SelectItem key={location.uuid} text={location.display} value={location.uuid}>
512
+ {location.display}
513
+ </SelectItem>
514
+ ))}
515
+ </Select>
627
516
  )}
628
- <ResponsiveWrapper>
629
- <Controller
630
- name="appointmentDateTime"
631
- control={control}
632
- render={({ field: { onChange, value }, fieldState }) => (
633
- <OpenmrsDateRangePicker
634
- value={
635
- value.startDate && value.recurringPatternEndDate
636
- ? [value.startDate, value.recurringPatternEndDate]
637
- : null
517
+ />
518
+ </ResponsiveWrapper>
519
+ </FormGroup>
520
+ <FormGroup className={styles.formGroup} legendText={t('service', 'Service')}>
521
+ <ResponsiveWrapper>
522
+ <Controller
523
+ name="selectedService"
524
+ control={control}
525
+ render={({ field: { onBlur, onChange, value, ref } }) => (
526
+ <Select
527
+ id="service"
528
+ invalid={!!errors?.selectedService}
529
+ invalidText={errors?.selectedService?.message}
530
+ labelText={t('selectService', 'Select a service')}
531
+ onBlur={onBlur}
532
+ onChange={(event: React.ChangeEvent<HTMLSelectElement>) => {
533
+ if (!isEditing) {
534
+ setValue(
535
+ 'duration',
536
+ services?.find((service) => service.name === event.target.value)?.durationMins,
537
+ );
538
+ } else {
539
+ const previousServiceDuration = services?.find(
540
+ (service) => service.name === getValues('selectedService'),
541
+ )?.durationMins;
542
+ const selectedServiceDuration = services?.find(
543
+ (service) => service.name === event.target.value,
544
+ )?.durationMins;
545
+ if (selectedServiceDuration && previousServiceDuration === getValues('duration')) {
546
+ setValue('duration', selectedServiceDuration);
638
547
  }
639
- onChange={(dateRange) => {
640
- const [startDate, endDate] = dateRange;
641
- onChange({
642
- ...value,
643
- startDate,
644
- startDateText: startDate ? dayjs(startDate).format(dateFormat) : '',
645
- recurringPatternEndDate: endDate,
646
- recurringPatternEndDateText: endDate ? dayjs(endDate).format(dateFormat) : '',
647
- });
648
- }}
649
- startName="start"
650
- endName="end"
651
- id="appointmentRecurringDateRangePicker"
652
- data-testid="appointmentRecurringDateRangePicker"
653
- labelText={t('dateRange', 'Set date range')}
654
- invalid={!!fieldState?.error?.message}
655
- invalidText={fieldState?.error?.message}
656
- isRequired
657
- />
658
- )}
659
- />
660
- </ResponsiveWrapper>
661
-
662
- {!watch('isAllDayAppointment') && <TimeAndDuration t={t} control={control} errors={errors} />}
663
-
664
- <ResponsiveWrapper>
665
- <Controller
666
- name="recurringPatternPeriod"
667
- control={control}
668
- render={({ field: { onBlur, onChange, value } }) => (
669
- <NumberInput
670
- hideSteppers
671
- id="repeatNumber"
672
- min={1}
673
- max={356}
674
- label={t('repeatEvery', 'Repeat every')}
675
- invalidText={t('invalidNumber', 'Number is not valid')}
676
- value={value}
677
- onBlur={onBlur}
678
- onChange={(e) => {
679
- onChange(Number(e.target.value));
680
- }}
681
- />
682
- )}
683
- />
684
- </ResponsiveWrapper>
685
-
686
- <ResponsiveWrapper>
687
- <Controller
688
- name="recurringPatternType"
689
- control={control}
690
- render={({ field: { onChange, value } }) => (
691
- <RadioButtonGroup
692
- legendText={t('period', 'Period')}
693
- name="radio-button-group"
694
- onChange={(type) => onChange(type)}
695
- valueSelected={value}>
696
- <RadioButton labelText={t('day', 'Day')} value="DAY" id="radioDay" />
697
- <RadioButton labelText={t('week', 'Week')} value="WEEK" id="radioWeek" />
698
- </RadioButtonGroup>
699
- )}
700
- />
701
- </ResponsiveWrapper>
548
+ }
549
+ onChange(event);
550
+ }}
551
+ ref={ref}
552
+ value={value}>
553
+ <SelectItem text={t('chooseService', 'Select service')} value="" />
554
+ {services?.length > 0 &&
555
+ services.map((service) => (
556
+ <SelectItem key={service.uuid} text={service.name} value={service.name}>
557
+ {service.name}
558
+ </SelectItem>
559
+ ))}
560
+ </Select>
561
+ )}
562
+ />
563
+ </ResponsiveWrapper>
564
+ </FormGroup>
565
+ <FormGroup className={styles.formGroup} legendText={t('appointmentType_title', 'Appointment Type')}>
566
+ <ResponsiveWrapper>
567
+ <Controller
568
+ name="appointmentType"
569
+ control={control}
570
+ render={({ field: { onBlur, onChange, value, ref } }) => (
571
+ <Select
572
+ disabled={!appointmentTypes?.length}
573
+ id="appointmentType"
574
+ invalid={!!errors?.appointmentType}
575
+ invalidText={errors?.appointmentType?.message}
576
+ labelText={t('selectAppointmentType', 'Select the type of appointment')}
577
+ onBlur={onBlur}
578
+ onChange={onChange}
579
+ ref={ref}
580
+ value={value}>
581
+ <SelectItem text={t('chooseAppointmentType', 'Choose appointment type')} value="" />
582
+ {appointmentTypes?.length > 0 &&
583
+ appointmentTypes.map((appointmentType, index) => (
584
+ <SelectItem key={index} text={appointmentType} value={appointmentType}>
585
+ {appointmentType}
586
+ </SelectItem>
587
+ ))}
588
+ </Select>
589
+ )}
590
+ />
591
+ </ResponsiveWrapper>
592
+ </FormGroup>
593
+
594
+ <FormGroup className={styles.formGroup} legendText={t('recurringAppointment', 'Recurring Appointment')}>
595
+ <div>
596
+ <Toggle
597
+ id="recurringToggle"
598
+ labelB={t('yes', 'Yes')}
599
+ labelA={t('no', 'No')}
600
+ labelText={t('isRecurringAppointment', 'Is this a recurring appointment?')}
601
+ onClick={() => setIsRecurringAppointment(!isRecurringAppointment)}
602
+ />
603
+ </div>
604
+ </FormGroup>
702
605
 
703
- {watch('recurringPatternType') === 'WEEK' && (
704
- <div>
606
+ <FormGroup className={styles.formGroup} legendText={t('dateTime', 'Date & Time')}>
607
+ <div className={styles.dateTimeFields}>
608
+ {isRecurringAppointment && (
609
+ <div className={styles.inputContainer}>
610
+ {allowAllDayAppointments && (
705
611
  <Controller
706
- name="selectedDaysOfWeekText"
612
+ name="isAllDayAppointment"
707
613
  control={control}
708
- defaultValue={defaultSelectedDaysOfWeekText}
709
- render={({ field: { onChange } }) => (
710
- <MultiSelect
711
- className={styles.weekSelect}
712
- id="daysOfWeek"
713
- initialSelectedItems={weekDays.filter((i) =>
714
- getValues('recurringPatternDaysOfWeek').includes(i.id),
715
- )}
716
- items={weekDays}
717
- itemToString={(item) => (item ? t(item.labelCode, item.label) : '')}
718
- label={getValues('selectedDaysOfWeekText')}
719
- onChange={(e) => {
720
- onChange(e);
721
- handleSelectChange(e);
614
+ render={({ field: { value, onChange } }) => (
615
+ <Toggle
616
+ id="allDayToggle"
617
+ labelA={t('no', 'No')}
618
+ labelB={t('yes', 'Yes')}
619
+ labelText={t('allDay', 'All day')}
620
+ toggled={value}
621
+ onToggle={onChange}
622
+ />
623
+ )}
624
+ />
625
+ )}
626
+ <ResponsiveWrapper>
627
+ <Controller
628
+ name="appointmentDateTime"
629
+ control={control}
630
+ render={({ field: { onChange, value }, fieldState }) => (
631
+ <OpenmrsDateRangePicker
632
+ value={
633
+ value.startDate && value.recurringPatternEndDate
634
+ ? [value.startDate, value.recurringPatternEndDate]
635
+ : null
636
+ }
637
+ onChange={(dateRange) => {
638
+ const [startDate, endDate] = dateRange;
639
+ onChange({
640
+ ...value,
641
+ startDate,
642
+ startDateText: startDate ? dayjs(startDate).format(dateFormat) : '',
643
+ recurringPatternEndDate: endDate,
644
+ recurringPatternEndDateText: endDate ? dayjs(endDate).format(dateFormat) : '',
645
+ });
722
646
  }}
723
- selectionFeedback="top-after-reopen"
724
- sortItems={(items) => {
725
- return items.sort((a, b) => a.order > b.order);
647
+ startName="start"
648
+ endName="end"
649
+ id="appointmentRecurringDateRangePicker"
650
+ data-testid="appointmentRecurringDateRangePicker"
651
+ labelText={t('dateRange', 'Set date range')}
652
+ invalid={!!fieldState?.error?.message}
653
+ invalidText={fieldState?.error?.message}
654
+ isRequired
655
+ />
656
+ )}
657
+ />
658
+ </ResponsiveWrapper>
659
+
660
+ {!watch('isAllDayAppointment') && <TimeAndDuration t={t} control={control} errors={errors} />}
661
+
662
+ <ResponsiveWrapper>
663
+ <Controller
664
+ name="recurringPatternPeriod"
665
+ control={control}
666
+ render={({ field: { onBlur, onChange, value } }) => (
667
+ <NumberInput
668
+ hideSteppers
669
+ id="repeatNumber"
670
+ min={1}
671
+ max={356}
672
+ label={t('repeatEvery', 'Repeat every')}
673
+ invalidText={t('invalidNumber', 'Number is not valid')}
674
+ value={value}
675
+ onBlur={onBlur}
676
+ onChange={(e) => {
677
+ onChange(Number(e.target.value));
726
678
  }}
727
679
  />
728
680
  )}
729
681
  />
730
- </div>
731
- )}
732
- </div>
733
- )}
734
-
735
- {!isRecurringAppointment && (
736
- <div className={styles.inputContainer}>
737
- {allowAllDayAppointments && (
738
- <Controller
739
- name="isAllDayAppointment"
740
- control={control}
741
- render={({ field: { value, onChange } }) => (
742
- <Toggle
743
- id="allDayToggle"
744
- labelA={t('no', 'No')}
745
- labelB={t('yes', 'Yes')}
746
- labelText={t('allDay', 'All day')}
747
- toggled={value}
748
- onToggle={onChange}
749
- />
750
- )}
751
- />
752
- )}
753
- <ResponsiveWrapper>
754
- <Controller
755
- name="appointmentDateTime"
756
- control={control}
757
- render={({ field, fieldState }) => (
758
- <OpenmrsDatePicker
759
- data-testid="datePickerInput"
760
- id="datePickerInput"
761
- invalid={!!fieldState?.error?.message}
762
- invalidText={fieldState?.error?.message}
763
- labelText={t('date', 'Date')}
764
- onBlur={field.onBlur}
765
- onChange={(date) => {
766
- field.onChange({
767
- ...field.value,
768
- startDate: date,
769
- });
770
- }}
771
- style={{ width: '100%' }}
772
- value={field.value.startDate}
682
+ </ResponsiveWrapper>
683
+
684
+ <ResponsiveWrapper>
685
+ <Controller
686
+ name="recurringPatternType"
687
+ control={control}
688
+ render={({ field: { onChange, value } }) => (
689
+ <RadioButtonGroup
690
+ legendText={t('period', 'Period')}
691
+ name="radio-button-group"
692
+ onChange={(type) => onChange(type)}
693
+ valueSelected={value}>
694
+ <RadioButton labelText={t('day', 'Day')} value="DAY" id="radioDay" />
695
+ <RadioButton labelText={t('week', 'Week')} value="WEEK" id="radioWeek" />
696
+ </RadioButtonGroup>
697
+ )}
698
+ />
699
+ </ResponsiveWrapper>
700
+
701
+ {watch('recurringPatternType') === 'WEEK' && (
702
+ <div>
703
+ <Controller
704
+ name="selectedDaysOfWeekText"
705
+ control={control}
706
+ defaultValue={defaultSelectedDaysOfWeekText}
707
+ render={({ field: { onChange } }) => (
708
+ <MultiSelect
709
+ className={styles.weekSelect}
710
+ id="daysOfWeek"
711
+ initialSelectedItems={weekDays.filter((i) =>
712
+ getValues('recurringPatternDaysOfWeek').includes(i.id),
713
+ )}
714
+ items={weekDays}
715
+ itemToString={(item) => (item ? t(item.labelCode, item.label) : '')}
716
+ label={getValues('selectedDaysOfWeekText')}
717
+ onChange={(e) => {
718
+ onChange(e);
719
+ handleSelectChange(e);
720
+ }}
721
+ selectionFeedback="top-after-reopen"
722
+ sortItems={(items) => {
723
+ return items.sort((a, b) => a.order > b.order);
724
+ }}
725
+ />
726
+ )}
773
727
  />
774
- )}
775
- />
776
- </ResponsiveWrapper>
728
+ </div>
729
+ )}
730
+ </div>
731
+ )}
777
732
 
778
- {!watch('isAllDayAppointment') && <TimeAndDuration t={t} control={control} errors={errors} />}
779
- </div>
780
- )}
781
- </div>
782
- </FormGroup>
733
+ {!isRecurringAppointment && (
734
+ <div className={styles.inputContainer}>
735
+ {allowAllDayAppointments && (
736
+ <Controller
737
+ name="isAllDayAppointment"
738
+ control={control}
739
+ render={({ field: { value, onChange } }) => (
740
+ <Toggle
741
+ id="allDayToggle"
742
+ labelA={t('no', 'No')}
743
+ labelB={t('yes', 'Yes')}
744
+ labelText={t('allDay', 'All day')}
745
+ toggled={value}
746
+ onToggle={onChange}
747
+ />
748
+ )}
749
+ />
750
+ )}
751
+ <ResponsiveWrapper>
752
+ <Controller
753
+ name="appointmentDateTime"
754
+ control={control}
755
+ render={({ field, fieldState }) => (
756
+ <OpenmrsDatePicker
757
+ data-testid="datePickerInput"
758
+ id="datePickerInput"
759
+ invalid={!!fieldState?.error?.message}
760
+ invalidText={fieldState?.error?.message}
761
+ labelText={t('date', 'Date')}
762
+ onBlur={field.onBlur}
763
+ onChange={(date) => {
764
+ field.onChange({
765
+ ...field.value,
766
+ startDate: date,
767
+ });
768
+ }}
769
+ style={{ width: '100%' }}
770
+ value={field.value.startDate}
771
+ />
772
+ )}
773
+ />
774
+ </ResponsiveWrapper>
783
775
 
784
- {getValues('selectedService') && (
785
- <FormGroup className={styles.formGroup} legendText="">
786
- <ResponsiveWrapper>
787
- <Workload
788
- appointmentDate={watch('appointmentDateTime').startDate}
789
- onWorkloadDateChange={handleWorkloadDateChange}
790
- selectedService={watch('selectedService')}
791
- />
792
- </ResponsiveWrapper>
776
+ {!watch('isAllDayAppointment') && <TimeAndDuration t={t} control={control} errors={errors} />}
777
+ </div>
778
+ )}
779
+ </div>
793
780
  </FormGroup>
794
- )}
795
781
 
796
- {context !== 'creating' ? (
797
- <FormGroup className={styles.formGroup} legendText={t('appointmentStatus', 'Appointment Status')}>
782
+ {getValues('selectedService') && (
783
+ <FormGroup className={styles.formGroup} legendText="">
784
+ <ResponsiveWrapper>
785
+ <Workload
786
+ appointmentDate={watch('appointmentDateTime').startDate}
787
+ onWorkloadDateChange={handleWorkloadDateChange}
788
+ selectedService={watch('selectedService')}
789
+ />
790
+ </ResponsiveWrapper>
791
+ </FormGroup>
792
+ )}
793
+
794
+ {isEditing ? (
795
+ <FormGroup className={styles.formGroup} legendText={t('appointmentStatus', 'Appointment Status')}>
796
+ <ResponsiveWrapper>
797
+ <Controller
798
+ name="appointmentStatus"
799
+ control={control}
800
+ render={({ field: { onBlur, onChange, value, ref } }) => (
801
+ <Select
802
+ id="appointmentStatus"
803
+ invalid={!!errors?.appointmentStatus}
804
+ invalidText={errors?.appointmentStatus?.message}
805
+ labelText={t('selectAppointmentStatus', 'Select status')}
806
+ onBlur={onBlur}
807
+ onChange={onChange}
808
+ ref={ref}
809
+ value={value}>
810
+ <SelectItem text={t('selectAppointmentStatus', 'Select status')} value="" />
811
+ {appointmentStatuses?.length > 0 &&
812
+ appointmentStatuses.map((appointmentStatus, index) => (
813
+ <SelectItem key={index} text={appointmentStatus} value={appointmentStatus}>
814
+ {appointmentStatus}
815
+ </SelectItem>
816
+ ))}
817
+ </Select>
818
+ )}
819
+ />
820
+ </ResponsiveWrapper>
821
+ </FormGroup>
822
+ ) : null}
823
+
824
+ <FormGroup className={styles.formGroup} legendText={t('provider', 'Provider')}>
798
825
  <ResponsiveWrapper>
799
826
  <Controller
800
- name="appointmentStatus"
827
+ name="provider"
801
828
  control={control}
802
- render={({ field: { onBlur, onChange, value, ref } }) => (
829
+ render={({ field: { onChange, value, onBlur, ref } }) => (
803
830
  <Select
804
- id="appointmentStatus"
805
- invalid={!!errors?.appointmentStatus}
806
- invalidText={errors?.appointmentStatus?.message}
807
- labelText={t('selectAppointmentStatus', 'Select status')}
808
- onBlur={onBlur}
831
+ id="provider"
832
+ invalidText="Required"
833
+ labelText={t('selectProvider', 'Select a provider')}
809
834
  onChange={onChange}
835
+ onBlur={onBlur}
810
836
  ref={ref}
811
837
  value={value}>
812
- <SelectItem text={t('selectAppointmentStatus', 'Select status')} value="" />
813
- {appointmentStatuses?.length > 0 &&
814
- appointmentStatuses.map((appointmentStatus, index) => (
815
- <SelectItem key={index} text={appointmentStatus} value={appointmentStatus}>
816
- {appointmentStatus}
838
+ <SelectItem text={t('chooseProvider', 'Choose a provider')} value="" />
839
+ {providers?.providers?.length > 0 &&
840
+ providers?.providers?.map((provider) => (
841
+ <SelectItem key={provider.uuid} text={provider.display} value={provider.uuid}>
842
+ {provider.display}
817
843
  </SelectItem>
818
844
  ))}
819
845
  </Select>
@@ -821,93 +847,66 @@ const AppointmentsForm: React.FC<AppointmentsFormProps & DefaultWorkspaceProps>
821
847
  />
822
848
  </ResponsiveWrapper>
823
849
  </FormGroup>
824
- ) : null}
825
-
826
- <FormGroup className={styles.formGroup} legendText={t('provider', 'Provider')}>
827
- <ResponsiveWrapper>
828
- <Controller
829
- name="provider"
830
- control={control}
831
- render={({ field: { onChange, value, onBlur, ref } }) => (
832
- <Select
833
- id="provider"
834
- invalidText="Required"
835
- labelText={t('selectProvider', 'Select a provider')}
836
- onChange={onChange}
837
- onBlur={onBlur}
838
- ref={ref}
839
- value={value}>
840
- <SelectItem text={t('chooseProvider', 'Choose a provider')} value="" />
841
- {providers?.providers?.length > 0 &&
842
- providers?.providers?.map((provider) => (
843
- <SelectItem key={provider.uuid} text={provider.display} value={provider.uuid}>
844
- {provider.display}
845
- </SelectItem>
846
- ))}
847
- </Select>
848
- )}
849
- />
850
- </ResponsiveWrapper>
851
- </FormGroup>
852
-
853
- <FormGroup
854
- className={styles.formGroup}
855
- legendText={t('dateAppointmentScheduled', 'Date appointment scheduled')}>
856
- <ResponsiveWrapper>
857
- <Controller
858
- name="dateAppointmentScheduled"
859
- control={control}
860
- render={({ field, fieldState }) => (
861
- <div style={{ width: '100%' }}>
862
- <OpenmrsDatePicker
863
- data-testid="dateAppointmentScheduledPickerInput"
864
- id="dateAppointmentScheduledPickerInput"
865
- invalid={!!fieldState?.error?.message}
866
- invalidText={fieldState?.error?.message}
867
- labelText={t('dateAppointmentIssued', 'Date appointment issued')}
868
- maxDate={new Date()}
869
- onBlur={field.onBlur}
870
- onChange={field.onChange}
871
- style={{ width: '100%' }}
872
- value={field.value}
850
+
851
+ <FormGroup
852
+ className={styles.formGroup}
853
+ legendText={t('dateAppointmentScheduled', 'Date appointment scheduled')}>
854
+ <ResponsiveWrapper>
855
+ <Controller
856
+ name="dateAppointmentScheduled"
857
+ control={control}
858
+ render={({ field, fieldState }) => (
859
+ <div style={{ width: '100%' }}>
860
+ <OpenmrsDatePicker
861
+ data-testid="dateAppointmentScheduledPickerInput"
862
+ id="dateAppointmentScheduledPickerInput"
863
+ invalid={!!fieldState?.error?.message}
864
+ invalidText={fieldState?.error?.message}
865
+ labelText={t('dateAppointmentIssued', 'Date appointment issued')}
866
+ maxDate={new Date()}
867
+ onBlur={field.onBlur}
868
+ onChange={field.onChange}
869
+ style={{ width: '100%' }}
870
+ value={field.value}
871
+ />
872
+ </div>
873
+ )}
874
+ />
875
+ </ResponsiveWrapper>
876
+ </FormGroup>
877
+
878
+ <FormGroup className={styles.formGroup} legendText={t('note', 'Note')}>
879
+ <ResponsiveWrapper>
880
+ <Controller
881
+ name="appointmentNote"
882
+ control={control}
883
+ render={({ field: { onChange, onBlur, value, ref } }) => (
884
+ <TextArea
885
+ enableCounter
886
+ id="appointmentNote"
887
+ value={value}
888
+ labelText={t('appointmentNoteLabel', 'Write an additional note')}
889
+ placeholder={t('appointmentNotePlaceholder', 'Write any additional points here')}
890
+ maxCount={255}
891
+ onChange={onChange}
892
+ onBlur={onBlur}
893
+ ref={ref}
873
894
  />
874
- </div>
875
- )}
876
- />
877
- </ResponsiveWrapper>
878
- </FormGroup>
879
-
880
- <FormGroup className={styles.formGroup} legendText={t('note', 'Note')}>
881
- <ResponsiveWrapper>
882
- <Controller
883
- name="appointmentNote"
884
- control={control}
885
- render={({ field: { onChange, onBlur, value, ref } }) => (
886
- <TextArea
887
- enableCounter
888
- id="appointmentNote"
889
- value={value}
890
- labelText={t('appointmentNoteLabel', 'Write an additional note')}
891
- placeholder={t('appointmentNotePlaceholder', 'Write any additional points here')}
892
- maxCount={255}
893
- onChange={onChange}
894
- onBlur={onBlur}
895
- ref={ref}
896
- />
897
- )}
898
- />
899
- </ResponsiveWrapper>
900
- </FormGroup>
901
- </Stack>
902
- <ButtonSet className={isTablet ? styles.tablet : styles.desktop}>
903
- <Button className={styles.button} onClick={closeWorkspace} kind="secondary">
904
- {t('discard', 'Discard')}
905
- </Button>
906
- <Button className={styles.button} disabled={isSubmitting} type="submit">
907
- {t('saveAndClose', 'Save and close')}
908
- </Button>
909
- </ButtonSet>
910
- </Form>
895
+ )}
896
+ />
897
+ </ResponsiveWrapper>
898
+ </FormGroup>
899
+ </Stack>
900
+ <ButtonSet className={isTablet ? styles.tablet : styles.desktop}>
901
+ <Button className={styles.button} onClick={closeWorkspace} kind="secondary">
902
+ {t('discard', 'Discard')}
903
+ </Button>
904
+ <Button className={styles.button} disabled={isSubmitting} type="submit">
905
+ {t('saveAndClose', 'Save and close')}
906
+ </Button>
907
+ </ButtonSet>
908
+ </Form>
909
+ </Workspace2>
911
910
  );
912
911
  };
913
912
 
@@ -937,7 +936,7 @@ function TimeAndDuration({ t, control, errors }: TimeAndDurationProps) {
937
936
  invalid={!!errors?.startTime}
938
937
  invalidText={errors?.startTime?.message}
939
938
  labelText={t('time', 'Time')}
940
- onChange={(event) => {
939
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
941
940
  onChange(event.target.value);
942
941
  }}
943
942
  style={{ marginLeft: '0.125rem', flex: 'none' }}
@@ -948,7 +947,9 @@ function TimeAndDuration({ t, control, errors }: TimeAndDurationProps) {
948
947
  render={({ field: { value, onChange } }) => (
949
948
  <TimePickerSelect
950
949
  id="time-picker-select-1"
951
- onChange={(event) => onChange(event.target.value as 'AM' | 'PM')}
950
+ onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
951
+ onChange(event.target.value as 'AM' | 'PM')
952
+ }
952
953
  value={value}
953
954
  aria-label={t('time', 'Time')}>
954
955
  <SelectItem value="AM" text="AM" />