@kenyaemr/esm-appointments-app 7.0.3-pre.89 → 7.0.3-pre.94

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 (112) hide show
  1. package/.turbo/turbo-build.log +23 -22
  2. package/dist/130.js +1 -1
  3. package/dist/130.js.map +1 -1
  4. package/dist/23.js +2 -0
  5. package/dist/23.js.map +1 -0
  6. package/dist/265.js +1 -1
  7. package/dist/271.js +1 -1
  8. package/dist/319.js +1 -1
  9. package/dist/460.js +1 -1
  10. package/dist/529.js +1 -1
  11. package/dist/574.js +1 -1
  12. package/dist/581.js +1 -0
  13. package/dist/581.js.map +1 -0
  14. package/dist/644.js +1 -1
  15. package/dist/646.js +2 -0
  16. package/dist/646.js.map +1 -0
  17. package/dist/757.js +1 -1
  18. package/dist/788.js +1 -1
  19. package/dist/807.js +1 -1
  20. package/dist/833.js +1 -1
  21. package/dist/85.js +1 -0
  22. package/dist/85.js.map +1 -0
  23. package/dist/89.js +1 -0
  24. package/dist/89.js.map +1 -0
  25. package/dist/kenyaemr-esm-appointments-app.js +1 -1
  26. package/dist/kenyaemr-esm-appointments-app.js.buildmanifest.json +167 -143
  27. package/dist/kenyaemr-esm-appointments-app.js.map +1 -1
  28. package/dist/main.js +1 -1
  29. package/dist/main.js.map +1 -1
  30. package/dist/routes.json +1 -1
  31. package/package.json +1 -1
  32. package/src/admin/appointment-services/appointment-services.scss +5 -5
  33. package/src/appointments/appointment-tabs.scss +6 -7
  34. package/src/appointments/appointment-tabs.test.tsx +4 -11
  35. package/src/appointments/common-components/appointments-actions.test.tsx +121 -74
  36. package/src/appointments/common-components/appointments-table.scss +6 -6
  37. package/src/appointments/common-components/appointments-table.test.tsx +30 -32
  38. package/src/appointments/common-components/end-appointment.test.tsx +20 -19
  39. package/src/appointments/details/appointment-details.component.tsx +1 -1
  40. package/src/appointments/details/appointment-details.scss +13 -13
  41. package/src/appointments/details/appointment-details.test.tsx +49 -48
  42. package/src/appointments/scheduled/scheduled-appointments.component.tsx +1 -1
  43. package/src/appointments/scheduled/scheduled-appointments.scss +3 -1
  44. package/src/appointments/unscheduled/unscheduled-appointments.test.tsx +35 -37
  45. package/src/appointments.component.tsx +0 -1
  46. package/src/calendar/appointments-calendar-view-view.scss +3 -4
  47. package/src/calendar/appointments-calendar-view.test.tsx +1 -5
  48. package/src/calendar/header/calendar-header.scss +4 -4
  49. package/src/calendar/monthly/days-of-week.scss +1 -1
  50. package/src/calendar/monthly/monthly-view-workload.scss +10 -10
  51. package/src/config-schema.ts +88 -90
  52. package/src/empty-state/empty-state.scss +4 -4
  53. package/src/form/appointments-form.component.tsx +58 -26
  54. package/src/form/appointments-form.resource.ts +2 -2
  55. package/src/form/appointments-form.scss +11 -11
  56. package/src/form/appointments-form.test.tsx +42 -43
  57. package/src/header/appointments-header.scss +12 -12
  58. package/src/home/home-appointments.scss +0 -1
  59. package/src/homepage-tile/appointments-tile.component.tsx +23 -0
  60. package/src/homepage-tile/appointments-tile.scss +39 -0
  61. package/src/homepage-tile/appointments.resource.ts +15 -0
  62. package/src/hooks/useClinicalMetrics.ts +1 -1
  63. package/src/hooks/useDefaultLocation.ts +1 -1
  64. package/src/hooks/usePatientAppointmentHistory.ts +1 -1
  65. package/src/hooks/useProviders.ts +1 -1
  66. package/src/index.ts +5 -0
  67. package/src/metrics/appointments-metrics.scss +0 -1
  68. package/src/metrics/appointments-metrics.test.tsx +25 -31
  69. package/src/metrics/metrics-card.component.tsx +6 -33
  70. package/src/metrics/metrics-card.scss +8 -8
  71. package/src/metrics/metrics-header.scss +1 -1
  72. package/src/past-visit/past-visit.component.tsx +1 -1
  73. package/src/past-visit/past-visit.resource.ts +1 -1
  74. package/src/past-visit/past-visit.scss +19 -14
  75. package/src/patient-appointments/patient-appointments-action-menu.scss +6 -0
  76. package/src/patient-appointments/patient-appointments-base.component.tsx +6 -6
  77. package/src/patient-appointments/patient-appointments-base.scss +24 -29
  78. package/src/patient-appointments/patient-appointments-base.test.tsx +13 -11
  79. package/src/patient-appointments/patient-appointments-header.scss +4 -5
  80. package/src/patient-appointments/patient-appointments-overview.component.tsx +2 -2
  81. package/src/patient-appointments/patient-appointments-overview.scss +0 -1
  82. package/src/patient-appointments/patient-appointments.resource.ts +1 -1
  83. package/src/patient-appointments/patient-upcoming-appointments-card.component.tsx +3 -3
  84. package/src/patient-appointments/patient-upcoming-appointments-card.scss +11 -10
  85. package/src/patient-search/patient-search.scss +15 -14
  86. package/src/routes.json +5 -0
  87. package/src/types/index.ts +4 -0
  88. package/src/workload/monthly-view-workload/monthly-workload.scss +21 -6
  89. package/src/workload/monthly-view-workload/monthlyWorkCard.tsx +2 -2
  90. package/src/workload/workload.scss +3 -3
  91. package/translations/am.json +2 -0
  92. package/translations/ar.json +2 -0
  93. package/translations/en.json +2 -0
  94. package/translations/es.json +2 -0
  95. package/translations/fr.json +30 -28
  96. package/translations/he.json +2 -0
  97. package/translations/km.json +2 -0
  98. package/translations/zh.json +2 -0
  99. package/translations/zh_CN.json +2 -0
  100. package/dist/224.js +0 -1
  101. package/dist/224.js.map +0 -1
  102. package/dist/445.js +0 -2
  103. package/dist/445.js.map +0 -1
  104. package/dist/857.js +0 -2
  105. package/dist/857.js.map +0 -1
  106. package/dist/904.js +0 -1
  107. package/dist/904.js.map +0 -1
  108. package/src/root.scss +0 -50
  109. /package/dist/{857.js.LICENSE.txt → 23.js.LICENSE.txt} +0 -0
  110. /package/dist/{445.js.LICENSE.txt → 646.js.LICENSE.txt} +0 -0
  111. /package/src/helpers/{time.tsx → time.ts} +0 -0
  112. /package/src/patient-appointments/{patient-appointments-table.tsx → patient-appointments-table.component.tsx} +0 -0
@@ -2,94 +2,31 @@ import { Type, restBaseUrl, validators } from '@openmrs/esm-framework';
2
2
  import { spaHomePage } from './constants';
3
3
 
4
4
  export const configSchema = {
5
- concepts: {
6
- visitQueueNumberAttributeUuid: {
7
- _type: Type.String,
8
- _description: 'The UUID of the visit attribute that contains the visit queue number.',
9
- _default: 'c61ce16f-272a-41e7-9924-4c555d0932c5',
10
- },
5
+ allowAllDayAppointments: {
6
+ _type: Type.Boolean,
7
+ _description: 'Whether to allow scheduling of all-day appointments (vs appointments with start time and end time)',
8
+ _default: false,
11
9
  },
12
- appointmentTypes: {
13
- _type: Type.Array,
14
- _description: 'Configurable appointment types (types of appointments)',
15
- _default: ['Scheduled'],
10
+ appointmentsBaseUrl: {
11
+ _type: Type.String,
12
+ _description: 'Configurable alternative URL for the Appointments UI. Eg, the Bahmni Appointments UI URL',
13
+ _default: `${spaHomePage}`,
16
14
  },
17
15
  appointmentStatuses: {
18
16
  _type: Type.Array,
19
17
  _description: 'Configurable appointment status (status of appointments)',
20
18
  _default: ['Requested', 'Scheduled', 'CheckedIn', 'Completed', 'Cancelled', 'Missed'],
21
19
  },
22
- daysOfTheWeek: {
20
+ appointmentTypes: {
23
21
  _type: Type.Array,
24
- _description: 'Configurable days of the week',
25
- _default: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
26
- },
27
- useBahmniAppointmentsUI: {
28
- _type: Type.Boolean,
29
- _description: 'Open links in Bahmni Appointments UI instead of O3 UI',
30
- _default: false,
31
- },
32
-
33
- useFullViewPrivilege: {
34
- _type: Type.Boolean,
35
- _description: "If set to 'false', will always display the full view, disregarding any privilege",
36
- _default: false,
37
- },
38
-
39
- fullViewPrivilege: {
40
- _type: Type.String,
41
- _description: 'Name of the privilege to display the full view of the Appointments dashboard widget.',
42
- _default: "Today's Appointments Widget: Display Full View",
22
+ _description: 'Configurable appointment types (types of appointments)',
23
+ _default: ['Scheduled'],
43
24
  },
44
25
  bahmniAppointmentsUiBaseUrl: {
45
26
  _type: Type.String,
46
27
  _description: 'Configurable base URL that points to the Bahmni Appointments UI',
47
28
  _default: '/appointments',
48
29
  },
49
- appointmentsBaseUrl: {
50
- _type: Type.String,
51
- _description: 'Configurable alternative URL for the Appointments UI. Eg, the Bahmni Appointments UI URL',
52
- _default: `${spaHomePage}`,
53
- },
54
- hiddenFormFields: {
55
- _type: Type.Array,
56
- _description: 'Array of form controls to be hidden on form load',
57
- _default: [],
58
- },
59
- showServiceQueueFields: {
60
- _type: Type.Boolean,
61
- _description: 'Whether start visit form should display service queue fields`',
62
- _default: false,
63
- },
64
- defaultFacilityUrl: {
65
- _type: Type.String,
66
- _default: `${restBaseUrl}/kenyaemr/default-facility`,
67
- _description: 'Custom URL to load default facility if it is not in the session',
68
- },
69
- customPatientChartUrl: {
70
- _type: Type.String,
71
- _description: `Template URL that will be used when clicking on the patient name in the queues table.
72
- Available argument: patientUuid, openmrsSpaBase, openBase
73
- (openmrsSpaBase and openBase are available to any <ConfigurableLink>)`,
74
- _default: '${openmrsSpaBase}/patient/${patientUuid}/chart',
75
- _validators: [validators.isUrlWithTemplateParameters(['patientUuid'])],
76
- },
77
- patientIdentifierType: {
78
- _type: Type.String,
79
- _description: 'The name of the patient identifier type to be used for the patient identifier field',
80
- _default: '',
81
- },
82
- showUnscheduledAppointmentsTab: {
83
- _type: Type.Boolean,
84
- _description:
85
- 'Whether to show the Unscheduled Appointments tab. Note that configuring this to true requires a custom unscheduledAppointment endpoint not currently available',
86
- _default: false,
87
- },
88
- allowAllDayAppointments: {
89
- _type: Type.Boolean,
90
- _description: 'Whether to allow scheduling of all-day appointments (vs appointments with start time and end time)',
91
- _default: false,
92
- },
93
30
  checkInButton: {
94
31
  enabled: {
95
32
  _type: Type.Boolean,
@@ -119,26 +56,74 @@ export const configSchema = {
119
56
  _default: '',
120
57
  },
121
58
  },
59
+ concepts: {
60
+ visitQueueNumberAttributeUuid: {
61
+ _type: Type.String,
62
+ _description: 'The UUID of the visit attribute that contains the visit queue number.',
63
+ _default: 'c61ce16f-272a-41e7-9924-4c555d0932c5',
64
+ },
65
+ },
66
+ customPatientChartUrl: {
67
+ _type: Type.String,
68
+ _description: `Template URL that will be used when clicking on the patient name in the queues table.
69
+ Available argument: patientUuid, openmrsSpaBase, openBase
70
+ (openmrsSpaBase and openBase are available to any <ConfigurableLink>)`,
71
+ _default: '${openmrsSpaBase}/patient/${patientUuid}/chart',
72
+ _validators: [validators.isUrlWithTemplateParameters(['patientUuid'])],
73
+ },
74
+ daysOfTheWeek: {
75
+ _type: Type.Array,
76
+ _description: 'Configurable days of the week',
77
+ _default: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
78
+ },
79
+ defaultFacilityUrl: {
80
+ _type: Type.String,
81
+ _default: `${restBaseUrl}/kenyaemr/default-facility`,
82
+ _description: 'Custom URL to load default facility if it is not in the session',
83
+ },
84
+ fullViewPrivilege: {
85
+ _type: Type.String,
86
+ _description: 'Name of the privilege to display the full view of the Appointments dashboard widget.',
87
+ _default: "Today's Appointments Widget: Display Full View",
88
+ },
89
+ hiddenFormFields: {
90
+ _type: Type.Array,
91
+ _description: 'Array of form controls to be hidden on form load',
92
+ _default: [],
93
+ },
94
+ patientIdentifierType: {
95
+ _type: Type.String,
96
+ _description: 'The name of the patient identifier type to be used for the patient identifier field',
97
+ _default: '',
98
+ },
99
+ showServiceQueueFields: {
100
+ _type: Type.Boolean,
101
+ _description: 'Whether start visit form should display service queue fields`',
102
+ _default: false,
103
+ },
104
+ showUnscheduledAppointmentsTab: {
105
+ _type: Type.Boolean,
106
+ _description:
107
+ 'Whether to show the Unscheduled Appointments tab. Note that configuring this to true requires a custom unscheduledAppointment endpoint not currently available',
108
+ _default: false,
109
+ },
110
+ useBahmniAppointmentsUI: {
111
+ _type: Type.Boolean,
112
+ _description: 'Open links in Bahmni Appointments UI instead of O3 UI',
113
+ _default: false,
114
+ },
115
+ useFullViewPrivilege: {
116
+ _type: Type.Boolean,
117
+ _description: "If set to 'false', will always display the full view, disregarding any privilege",
118
+ _default: false,
119
+ },
122
120
  };
123
121
 
124
122
  export interface ConfigObject {
125
- concepts: {
126
- visitQueueNumberAttributeUuid: string;
127
- };
128
- appointmentTypes: Array<string>;
129
- daysOfTheWeek: Array<string>;
130
- appointmentStatuses: Array<string>;
131
- useBahmniAppointmentsUI: boolean;
132
- useFullViewPrivilege: boolean;
133
- fullViewPrivilege: string;
134
- appointmentComments: Array<string>;
135
- hiddenFormFields: Array<string>;
136
- showServiceQueueFields: boolean;
137
- defaultFacilityUrl: string;
138
- customPatientChartUrl: string;
139
- patientIdentifierType: string;
140
- showUnscheduledAppointmentsTab: boolean;
141
123
  allowAllDayAppointments: boolean;
124
+ appointmentComments: Array<string>;
125
+ appointmentStatuses: Array<string>;
126
+ appointmentTypes: Array<string>;
142
127
  checkInButton: {
143
128
  enabled: boolean;
144
129
  showIfActiveVisit: boolean;
@@ -148,4 +133,17 @@ export interface ConfigObject {
148
133
  enabled: boolean;
149
134
  customUrl: string;
150
135
  };
136
+ concepts: {
137
+ visitQueueNumberAttributeUuid: string;
138
+ };
139
+ customPatientChartUrl: string;
140
+ daysOfTheWeek: Array<string>;
141
+ defaultFacilityUrl: string;
142
+ fullViewPrivilege: string;
143
+ hiddenFormFields: Array<string>;
144
+ patientIdentifierType: string;
145
+ showServiceQueueFields: boolean;
146
+ showUnscheduledAppointmentsTab: boolean;
147
+ useBahmniAppointmentsUI: boolean;
148
+ useFullViewPrivilege: boolean;
151
149
  }
@@ -1,6 +1,6 @@
1
1
  @use '@carbon/layout';
2
2
  @use '@carbon/type';
3
- @import '~@openmrs/esm-styleguide/src/vars';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
4
 
5
5
  .content {
6
6
  @include type.type-style('heading-compact-01');
@@ -36,7 +36,7 @@
36
36
  h4:after {
37
37
  content: '';
38
38
  display: block;
39
- width: 2rem;
39
+ width: layout.$spacing-07;
40
40
  padding-top: 0.188rem;
41
41
  border-bottom: 0.375rem solid var(--brand-03);
42
42
  }
@@ -45,7 +45,7 @@
45
45
  .heading:after {
46
46
  content: '';
47
47
  display: block;
48
- width: 2rem;
48
+ width: layout.$spacing-07;
49
49
  padding-top: 0.188rem;
50
50
  border-bottom: 0.375rem solid var(--brand-03);
51
51
  }
@@ -57,7 +57,7 @@
57
57
 
58
58
  .header4 {
59
59
  @include type.type-style('heading-compact-01');
60
- margin-bottom: 1rem;
60
+ margin-bottom: layout.$spacing-05;
61
61
  }
62
62
 
63
63
  // Overriding styles for RTL support
@@ -43,7 +43,6 @@ import {
43
43
  useMutateAppointments,
44
44
  } from './appointments-form.resource';
45
45
  import { useProviders } from '../hooks/useProviders';
46
- import Workload from '../workload/workload.component';
47
46
  import type { Appointment, AppointmentPayload, RecurringPattern } from '../types';
48
47
  import { type ConfigObject } from '../config-schema';
49
48
  import {
@@ -54,9 +53,9 @@ import {
54
53
  moduleName,
55
54
  weekDays,
56
55
  } from '../constants';
57
- import styles from './appointments-form.scss';
58
56
  import SelectedDateContext from '../hooks/selectedDateContext';
59
- import uniqBy from 'lodash/uniqBy';
57
+ import Workload from '../workload/workload.component';
58
+ import styles from './appointments-form.scss';
60
59
 
61
60
  const time12HourFormatRegexPattern = '^(1[0-2]|0?[1-9]):[0-5][0-9]$';
62
61
 
@@ -139,12 +138,20 @@ const AppointmentsForm: React.FC<AppointmentsFormProps> = ({
139
138
  .refine((duration) => (isAllDayAppointment ? true : duration > 0), {
140
139
  message: translateFrom(moduleName, 'durationErrorMessage', 'Duration should be greater than zero'),
141
140
  }),
142
- location: z.string().refine((value) => value !== ''),
143
- provider: z.string().refine((value) => value !== ''),
141
+ location: z.string().refine((value) => value !== '', {
142
+ message: translateFrom(moduleName, 'locationRequired', 'Location is required'),
143
+ }),
144
+ provider: z.string().refine((value) => value !== '', {
145
+ message: translateFrom(moduleName, 'providerRequired', 'Provider is required'),
146
+ }),
144
147
  appointmentStatus: z.string().optional(),
145
148
  appointmentNote: z.string(),
146
- appointmentType: z.string().refine((value) => value !== ''),
147
- selectedService: z.string().refine((value) => value !== ''),
149
+ appointmentType: z.string().refine((value) => value !== '', {
150
+ message: translateFrom(moduleName, 'appointmentTypeRequired', 'Appointment type is required'),
151
+ }),
152
+ selectedService: z.string().refine((value) => value !== '', {
153
+ message: translateFrom(moduleName, 'serviceRequired', 'Service is required'),
154
+ }),
148
155
  recurringPatternType: z.enum(['DAY', 'WEEK']),
149
156
  recurringPatternPeriod: z.number(),
150
157
  recurringPatternDaysOfWeek: z.array(z.string()),
@@ -179,7 +186,14 @@ const AppointmentsForm: React.FC<AppointmentsFormProps> = ({
179
186
  ? new Date(appointment?.dateAppointmentScheduled)
180
187
  : new Date();
181
188
 
182
- const { control, getValues, setValue, watch, handleSubmit } = useForm<AppointmentFormData>({
189
+ const {
190
+ control,
191
+ getValues,
192
+ setValue,
193
+ watch,
194
+ handleSubmit,
195
+ formState: { errors },
196
+ } = useForm<AppointmentFormData>({
183
197
  mode: 'all',
184
198
  resolver: zodResolver(appointmentsFormSchema),
185
199
  defaultValues: {
@@ -427,15 +441,16 @@ const AppointmentsForm: React.FC<AppointmentsFormProps> = ({
427
441
  <Controller
428
442
  name="location"
429
443
  control={control}
430
- render={({ field: { onChange, value, onBlur, ref } }) => (
444
+ render={({ field: { onChange, value, onBlur, ref }, fieldState }) => (
431
445
  <Select
432
446
  id="location"
433
- invalidText="Required"
434
447
  labelText={t('selectALocation', 'Select a location')}
435
448
  onChange={onChange}
436
449
  onBlur={onBlur}
437
450
  value={value}
438
- ref={ref}>
451
+ ref={ref}
452
+ invalid={!!fieldState?.error?.message}
453
+ invalidText={fieldState?.error?.message}>
439
454
  <SelectItem text={t('chooseLocation', 'Choose a location')} value="" />
440
455
  {locations?.length > 0 &&
441
456
  locations.map((location) => (
@@ -454,13 +469,15 @@ const AppointmentsForm: React.FC<AppointmentsFormProps> = ({
454
469
  <Controller
455
470
  name="dateAppointmentScheduled"
456
471
  control={control}
457
- render={({ field: { onChange, value, ref } }) => (
472
+ render={({ field: { onChange, value, ref }, fieldState }) => (
458
473
  <DatePicker
459
474
  datePickerType="single"
460
475
  dateFormat={datePickerFormat}
461
476
  value={value}
462
477
  maxDate={new Date()}
463
- onChange={([date]) => onChange(date)}>
478
+ onChange={([date]) => onChange(date)}
479
+ invalid={!!fieldState?.error?.message}
480
+ invalidText={fieldState?.error?.message}>
464
481
  <DatePickerInput
465
482
  id="dateAppointmentScheduledPickerInput"
466
483
  labelText={t('dateScheduledDetail', 'Date appointment issued')}
@@ -479,21 +496,33 @@ const AppointmentsForm: React.FC<AppointmentsFormProps> = ({
479
496
  <Controller
480
497
  name="selectedService"
481
498
  control={control}
482
- render={({ field: { onBlur, onChange, value, ref } }) => (
499
+ render={({ field: { onBlur, onChange, value, ref }, fieldState }) => (
483
500
  <Select
484
501
  id="service"
485
- invalidText="Required"
486
502
  labelText={t('selectService', 'Select a service')}
487
503
  onChange={(event) => {
504
+ if (context === 'creating') {
505
+ setValue(
506
+ 'duration',
507
+ services?.find((service) => service.name === event.target.value)?.durationMins,
508
+ );
509
+ } else if (context === 'editing') {
510
+ const previousServiceDuration = services?.find(
511
+ (service) => service.name === getValues('selectedService'),
512
+ )?.durationMins;
513
+ const selectedServiceDuration = services?.find((service) => service.name === event.target.value)
514
+ ?.durationMins;
515
+ if (selectedServiceDuration && previousServiceDuration === getValues('duration')) {
516
+ setValue('duration', selectedServiceDuration);
517
+ }
518
+ }
488
519
  onChange(event);
489
- setValue(
490
- 'duration',
491
- services?.find((service) => service.name === event.target.value)?.durationMins,
492
- );
493
520
  }}
494
521
  onBlur={onBlur}
495
522
  value={value}
496
- ref={ref}>
523
+ ref={ref}
524
+ invalid={!!fieldState?.error?.message}
525
+ invalidText={fieldState?.error?.message}>
497
526
  <SelectItem text={t('chooseService', 'Select service')} value="" />
498
527
  {services?.length > 0 &&
499
528
  services.map((service) => (
@@ -513,16 +542,17 @@ const AppointmentsForm: React.FC<AppointmentsFormProps> = ({
513
542
  <Controller
514
543
  name="appointmentType"
515
544
  control={control}
516
- render={({ field: { onBlur, onChange, value, ref } }) => (
545
+ render={({ field: { onBlur, onChange, value, ref }, fieldState }) => (
517
546
  <Select
518
547
  disabled={!appointmentTypes?.length}
519
548
  id="appointmentType"
520
- invalidText="Required"
521
549
  labelText={t('selectAppointmentType', 'Select the type of appointment')}
522
550
  onChange={onChange}
523
551
  value={value}
524
552
  ref={ref}
525
- onBlur={onBlur}>
553
+ onBlur={onBlur}
554
+ invalid={!!fieldState?.error?.message}
555
+ invalidText={fieldState?.error?.message}>
526
556
  <SelectItem text={t('chooseAppointmentType', 'Choose appointment type')} value="" />
527
557
  {appointmentTypes?.length > 0 &&
528
558
  appointmentTypes.map((appointmentType, index) => (
@@ -740,10 +770,11 @@ const AppointmentsForm: React.FC<AppointmentsFormProps> = ({
740
770
  <Controller
741
771
  name="appointmentStatus"
742
772
  control={control}
743
- render={({ field: { onBlur, onChange, value, ref } }) => (
773
+ render={({ field: { onBlur, onChange, value, ref }, fieldState }) => (
744
774
  <Select
745
775
  id="appointmentStatus"
746
- invalidText="Required"
776
+ invalid={!!fieldState?.error?.message}
777
+ invalidText={fieldState?.error?.message}
747
778
  labelText={t('selectAppointmentStatus', 'Select status')}
748
779
  onChange={onChange}
749
780
  value={value}
@@ -866,7 +897,7 @@ function TimeAndDuration({ isTablet, t, watch, control, services }) {
866
897
  name="duration"
867
898
  control={control}
868
899
  defaultValue={defaultDuration}
869
- render={({ field: { onChange, onBlur, value, ref } }) => (
900
+ render={({ field: { onChange, onBlur, value, ref }, fieldState }) => (
870
901
  <NumberInput
871
902
  hideSteppers
872
903
  disableWheel
@@ -880,6 +911,7 @@ function TimeAndDuration({ isTablet, t, watch, control, services }) {
880
911
  onChange={(event) => onChange(Number(event.target.value))}
881
912
  value={value}
882
913
  ref={ref}
914
+ invalid={fieldState?.error?.message}
883
915
  />
884
916
  )}
885
917
  />
@@ -77,7 +77,7 @@ export function useAppointments(patientUuid: string, startDate: string, abortCon
77
77
 
78
78
  return {
79
79
  data: data ? { pastAppointments, upcomingAppointments, todaysAppointments } : null,
80
- isError: error,
80
+ error,
81
81
  isLoading,
82
82
  isValidating,
83
83
  mutate,
@@ -92,7 +92,7 @@ export function useAppointmentService() {
92
92
 
93
93
  return {
94
94
  data: data ? data.data : null,
95
- isError: error,
95
+ error,
96
96
  isLoading,
97
97
  };
98
98
  }
@@ -1,13 +1,13 @@
1
- @use '@carbon/styles/scss/colors';
2
- @use '@carbon/styles/scss/spacing';
3
- @use '@carbon/styles/scss/type';
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
4
 
5
5
  $openmrs-background-grey: #ededed;
6
6
 
7
7
  .formGroup {
8
8
  display: flex;
9
- margin-bottom: spacing.$spacing-02;
10
- padding: spacing.$spacing-05;
9
+ margin-bottom: layout.$spacing-02;
10
+ padding: layout.$spacing-05;
11
11
  }
12
12
 
13
13
  :global(.omrs-breakpoint-gt-tablet) .formGroup {
@@ -29,13 +29,13 @@ $openmrs-background-grey: #ededed;
29
29
  .heading {
30
30
  color: colors.$gray-70;
31
31
  @include type.type-style('heading-compact-02');
32
- margin-bottom: spacing.$spacing-03;
32
+ margin-bottom: layout.$spacing-03;
33
33
  }
34
34
 
35
35
  .inputContainer {
36
36
  display: flex;
37
37
  flex-flow: row wrap;
38
- gap: spacing.$spacing-05;
38
+ gap: layout.$spacing-05;
39
39
  align-items: center;
40
40
  }
41
41
 
@@ -48,7 +48,7 @@ $openmrs-background-grey: #ededed;
48
48
  }
49
49
 
50
50
  .button {
51
- height: 4rem;
51
+ height: layout.$spacing-10;
52
52
  display: flex;
53
53
  align-content: flex-start;
54
54
  align-items: baseline;
@@ -59,12 +59,12 @@ $openmrs-background-grey: #ededed;
59
59
  display: flex;
60
60
  background-color: $openmrs-background-grey;
61
61
  justify-content: center;
62
- min-height: spacing.$spacing-09;
62
+ min-height: layout.$spacing-09;
63
63
  height: 100vh;
64
64
  }
65
65
 
66
66
  .tablet {
67
- padding: spacing.$spacing-06 spacing.$spacing-05;
67
+ padding: layout.$spacing-06 layout.$spacing-05;
68
68
  background-color: colors.$white;
69
69
  }
70
70
 
@@ -74,5 +74,5 @@ $openmrs-background-grey: #ededed;
74
74
 
75
75
  .weekSelect {
76
76
  max-inline-size: fit-content;
77
- padding-top: spacing.$spacing-03;
77
+ padding-top: layout.$spacing-03;
78
78
  }
@@ -1,34 +1,32 @@
1
1
  import React from 'react';
2
- import { screen } from '@testing-library/react';
3
2
  import userEvent from '@testing-library/user-event';
4
- import { mockLocations } from '__mocks__';
5
- import { openmrsFetch } from '@openmrs/esm-framework';
6
- import { mockUseAppointmentServiceData, mockSession } from '__mocks__';
3
+ import { screen } from '@testing-library/react';
4
+ import {
5
+ type FetchResponse,
6
+ getDefaultsFromConfigSchema,
7
+ openmrsFetch,
8
+ useConfig,
9
+ useLocations,
10
+ useSession,
11
+ } from '@openmrs/esm-framework';
12
+ import { mockUseAppointmentServiceData, mockSession, mockLocations } from '__mocks__';
7
13
  import { mockPatient, renderWithSwr, waitForLoadingToFinish } from 'tools';
8
14
  import { saveAppointment } from './appointments-form.resource';
15
+ import { configSchema, type ConfigObject } from '../config-schema';
9
16
  import AppointmentForm from './appointments-form.component';
10
17
 
11
- const testProps = {
18
+ const defaultProps = {
19
+ context: 'creating',
12
20
  closeWorkspace: jest.fn(),
13
21
  patientUuid: mockPatient.id,
14
22
  promptBeforeClosing: jest.fn(),
15
23
  };
16
24
 
17
- const mockCreateAppointment = saveAppointment as jest.Mock;
18
- const mockOpenmrsFetch = openmrsFetch as jest.Mock;
19
-
20
- jest.mock('@openmrs/esm-framework', () => {
21
- const originalModule = jest.requireActual('@openmrs/esm-framework');
22
-
23
- return {
24
- ...originalModule,
25
- useLocations: jest.fn().mockImplementation(() => mockLocations.data.results),
26
- useSession: jest.fn().mockImplementation(() => mockSession.data),
27
- useConfig: jest.fn(() => ({
28
- appointmentTypes: ['Scheduled', 'WalkIn'],
29
- })),
30
- };
31
- });
25
+ const mockCreateAppointment = jest.mocked(saveAppointment);
26
+ const mockOpenmrsFetch = jest.mocked(openmrsFetch);
27
+ const mockUseConfig = jest.mocked(useConfig<ConfigObject>);
28
+ const mockUseLocations = jest.mocked(useLocations);
29
+ const mockUseSession = jest.mocked(useSession);
32
30
 
33
31
  jest.mock('react-hook-form', () => ({
34
32
  ...jest.requireActual('react-hook-form'),
@@ -89,20 +87,25 @@ jest.mock('react-hook-form', () => ({
89
87
  }),
90
88
  }));
91
89
 
92
- jest.mock('./appointments-form.resource', () => {
93
- const originalModule = jest.requireActual('./appointments-form.resource');
94
-
95
- return {
96
- ...originalModule,
97
- saveAppointment: jest.fn(),
98
- };
99
- });
90
+ jest.mock('./appointments-form.resource', () => ({
91
+ ...jest.requireActual('./appointments-form.resource'),
92
+ saveAppointment: jest.fn(),
93
+ }));
100
94
 
101
95
  describe('AppointmentForm', () => {
96
+ beforeEach(() => {
97
+ mockUseConfig.mockReturnValue({
98
+ ...getDefaultsFromConfigSchema(configSchema),
99
+ appointmentTypes: ['Scheduled', 'WalkIn'],
100
+ });
101
+ mockUseLocations.mockReturnValue(mockLocations.data.results);
102
+ mockUseSession.mockReturnValue(mockSession.data);
103
+ });
104
+
102
105
  it('renders the appointments form showing all the relevant fields and values', async () => {
103
- mockOpenmrsFetch.mockReturnValue(mockUseAppointmentServiceData);
106
+ mockOpenmrsFetch.mockResolvedValue(mockUseAppointmentServiceData as unknown as FetchResponse);
104
107
 
105
- renderAppointmentsForm();
108
+ renderWithSwr(<AppointmentForm {...defaultProps} />);
106
109
 
107
110
  await waitForLoadingToFinish();
108
111
 
@@ -129,25 +132,25 @@ describe('AppointmentForm', () => {
129
132
  it('closes the form and the workspace when the cancel button is clicked', async () => {
130
133
  const user = userEvent.setup();
131
134
 
132
- mockOpenmrsFetch.mockReturnValueOnce(mockUseAppointmentServiceData);
135
+ mockOpenmrsFetch.mockResolvedValueOnce(mockUseAppointmentServiceData as unknown as FetchResponse);
133
136
 
134
- renderAppointmentsForm();
137
+ renderWithSwr(<AppointmentForm {...defaultProps} />);
135
138
 
136
139
  await waitForLoadingToFinish();
137
140
 
138
141
  const cancelButton = screen.getByRole('button', { name: /Discard/i });
139
142
  await user.click(cancelButton);
140
143
 
141
- expect(testProps.closeWorkspace).toHaveBeenCalledTimes(1);
144
+ expect(defaultProps.closeWorkspace).toHaveBeenCalledTimes(1);
142
145
  });
143
146
 
144
147
  it('renders a success snackbar upon successfully scheduling an appointment', async () => {
145
148
  const user = userEvent.setup();
146
149
 
147
- mockOpenmrsFetch.mockReturnValue({ data: mockUseAppointmentServiceData });
148
- mockCreateAppointment.mockResolvedValue({ status: 200, statusText: 'Ok' });
150
+ mockOpenmrsFetch.mockResolvedValue({ data: mockUseAppointmentServiceData } as unknown as FetchResponse);
151
+ mockCreateAppointment.mockResolvedValue({ status: 200, statusText: 'Ok' } as FetchResponse);
149
152
 
150
- renderAppointmentsForm();
153
+ renderWithSwr(<AppointmentForm {...defaultProps} />);
151
154
 
152
155
  await waitForLoadingToFinish();
153
156
 
@@ -158,7 +161,7 @@ describe('AppointmentForm', () => {
158
161
  const serviceSelect = screen.getByRole('combobox', { name: /Select a service/i });
159
162
  const appointmentTypeSelect = screen.getByRole('combobox', { name: /Select the type of appointment/i });
160
163
 
161
- expect(saveButton).not.toBeDisabled();
164
+ expect(saveButton).toBeEnabled();
162
165
 
163
166
  await user.clear(dateInput);
164
167
  await user.type(dateInput, '4/4/2021');
@@ -182,10 +185,10 @@ describe('AppointmentForm', () => {
182
185
  },
183
186
  };
184
187
 
185
- mockOpenmrsFetch.mockReturnValueOnce({ data: mockUseAppointmentServiceData });
188
+ mockOpenmrsFetch.mockResolvedValueOnce({ data: mockUseAppointmentServiceData } as unknown as FetchResponse);
186
189
  mockCreateAppointment.mockRejectedValueOnce(error);
187
190
 
188
- renderAppointmentsForm();
191
+ renderWithSwr(<AppointmentForm {...defaultProps} />);
189
192
 
190
193
  await waitForLoadingToFinish();
191
194
 
@@ -206,7 +209,3 @@ describe('AppointmentForm', () => {
206
209
  await user.click(saveButton);
207
210
  });
208
211
  });
209
-
210
- function renderAppointmentsForm() {
211
- renderWithSwr(<AppointmentForm {...testProps} />);
212
- }