@openmrs/esm-patient-immunizations-app 11.3.1-patch.9064 → 11.3.1-patch.9310

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 (44) hide show
  1. package/.turbo/turbo-build.log +20 -17
  2. package/dist/1268.js +2 -0
  3. package/dist/1268.js.map +1 -0
  4. package/dist/3176.js +1 -0
  5. package/dist/3606.js +1 -1
  6. package/dist/3606.js.map +1 -1
  7. package/dist/3677.js +1 -1
  8. package/dist/3677.js.map +1 -1
  9. package/dist/3679.js +1 -1
  10. package/dist/4055.js +1 -1
  11. package/dist/5670.js +1 -1
  12. package/dist/5670.js.map +1 -1
  13. package/dist/6336.js +1 -0
  14. package/dist/6336.js.map +1 -0
  15. package/dist/795.js +2 -1
  16. package/dist/795.js.map +1 -0
  17. package/dist/7955.js +1 -0
  18. package/dist/7955.js.map +1 -0
  19. package/dist/main.js +1 -1
  20. package/dist/main.js.map +1 -1
  21. package/dist/openmrs-esm-patient-immunizations-app.js +1 -1
  22. package/dist/openmrs-esm-patient-immunizations-app.js.buildmanifest.json +119 -140
  23. package/dist/openmrs-esm-patient-immunizations-app.js.map +1 -1
  24. package/dist/routes.json +1 -1
  25. package/package.json +3 -3
  26. package/src/immunizations/immunizations-detailed-summary.component.tsx +12 -7
  27. package/src/immunizations/immunizations-detailed-summary.test.tsx +41 -4
  28. package/src/immunizations/immunizations-form.test.tsx +166 -25
  29. package/src/immunizations/immunizations-form.workspace.tsx +155 -160
  30. package/src/immunizations/immunizations-overview.component.tsx +2 -2
  31. package/src/routes.json +13 -7
  32. package/translations/fr.json +5 -5
  33. package/dist/1323.js +0 -2
  34. package/dist/1323.js.map +0 -1
  35. package/dist/2537.js +0 -1
  36. package/dist/2537.js.map +0 -1
  37. package/dist/4918.js +0 -1
  38. package/dist/4918.js.map +0 -1
  39. package/dist/5858.js +0 -1
  40. package/dist/5858.js.map +0 -1
  41. package/dist/6783.js +0 -2
  42. package/dist/6783.js.map +0 -1
  43. /package/dist/{6783.js.LICENSE.txt → 1268.js.LICENSE.txt} +0 -0
  44. /package/dist/{1323.js.LICENSE.txt → 795.js.LICENSE.txt} +0 -0
@@ -20,14 +20,19 @@ import { orderBy } from 'lodash-es';
20
20
  import {
21
21
  AddIcon,
22
22
  formatDate,
23
- launchWorkspace,
24
23
  parseDate,
25
24
  useConfig,
26
25
  useLayoutType,
27
26
  usePagination,
28
- useVisit,
27
+ launchWorkspace2,
29
28
  } from '@openmrs/esm-framework';
30
- import { CardHeader, EmptyState, ErrorState, PatientChartPagination } from '@openmrs/esm-patient-common-lib';
29
+ import {
30
+ CardHeader,
31
+ EmptyState,
32
+ ErrorState,
33
+ PatientChartPagination,
34
+ usePatientChartStore,
35
+ } from '@openmrs/esm-patient-common-lib';
31
36
  import { immunizationFormSub, latestFirst, linkConfiguredSequences } from './utils';
32
37
  import { useImmunizations } from '../hooks/useImmunizations';
33
38
  import SequenceTable from './components/immunizations-sequence-table.component';
@@ -46,7 +51,7 @@ const ImmunizationsDetailedSummary: React.FC<ImmunizationsDetailedSummaryProps>
46
51
  const { immunizationsConfig } = useConfig();
47
52
  const displayText = t('immunizations__lower', 'immunizations');
48
53
  const headerTitle = t('immunizations', 'Immunizations');
49
- const { currentVisit } = useVisit(patientUuid);
54
+ const { visitContext } = usePatientChartStore(patientUuid);
50
55
  const isTablet = useLayoutType() === 'tablet';
51
56
  const sequenceDefinitions = immunizationsConfig?.sequenceDefinitions;
52
57
 
@@ -57,12 +62,12 @@ const ImmunizationsDetailedSummary: React.FC<ImmunizationsDetailedSummaryProps>
57
62
  }, [existingImmunizations, sequenceDefinitions]);
58
63
 
59
64
  const launchImmunizationsForm = useCallback(() => {
60
- if (!currentVisit) {
65
+ if (!visitContext) {
61
66
  launchStartVisitPrompt();
62
67
  return;
63
68
  }
64
- launchWorkspace('immunization-form-workspace');
65
- }, [currentVisit, launchStartVisitPrompt]);
69
+ launchWorkspace2('immunization-form-workspace');
70
+ }, [visitContext, launchStartVisitPrompt]);
66
71
 
67
72
  const sortedImmunizations = useMemo(() => {
68
73
  return orderBy(
@@ -1,19 +1,31 @@
1
1
  import React from 'react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import { screen } from '@testing-library/react';
4
- import { getDefaultsFromConfigSchema, launchWorkspace, useVisit, type VisitReturnType } from '@openmrs/esm-framework';
4
+ import { getDefaultsFromConfigSchema, launchWorkspace2, useVisit, type VisitReturnType } from '@openmrs/esm-framework';
5
5
  import { configSchema } from '../config-schema';
6
6
  import { mockPatient, renderWithSwr, waitForLoadingToFinish } from 'tools';
7
7
  import ImmunizationsDetailedSummary from './immunizations-detailed-summary.component';
8
+ import { usePatientChartStore } from '@openmrs/esm-patient-common-lib';
9
+ import { mockCurrentVisit } from '__mocks__';
8
10
 
9
11
  jest.mock('../hooks/useImmunizations', () => ({
10
12
  useImmunizations: jest.fn(),
11
13
  }));
14
+ jest.mock('@openmrs/esm-patient-common-lib', () => ({
15
+ ...jest.requireActual('@openmrs/esm-patient-common-lib'),
16
+ usePatientChartStore: jest.fn(),
17
+ }));
18
+
19
+ jest.mock('@openmrs/esm-patient-common-lib', () => ({
20
+ ...jest.requireActual('@openmrs/esm-patient-common-lib'),
21
+ usePatientChartStore: jest.fn(),
22
+ }));
12
23
 
13
24
  const mockUseImmunizations = jest.mocked(require('../hooks/useImmunizations').useImmunizations);
14
- const mockLaunchWorkspace = launchWorkspace as jest.Mock;
25
+ const mockLaunchWorkspace = launchWorkspace2 as jest.Mock;
15
26
  const mockUseVisit = jest.mocked(useVisit);
16
27
  const mockUseConfig = jest.mocked(require('@openmrs/esm-framework').useConfig);
28
+ const mockUsePatientChartStore = jest.mocked(usePatientChartStore);
17
29
 
18
30
  mockUseConfig.mockReturnValue({
19
31
  ...getDefaultsFromConfigSchema(configSchema),
@@ -74,6 +86,15 @@ describe('ImmunizationsDetailedSummary', () => {
74
86
  ],
75
87
  },
76
88
  });
89
+ mockUseVisit.mockReturnValue({ currentVisit: null } as VisitReturnType);
90
+ mockUsePatientChartStore.mockReturnValue({
91
+ patientUuid: 'patient-123',
92
+ patient: null,
93
+ visitContext: null,
94
+ mutateVisitContext: jest.fn(),
95
+ setPatient: jest.fn(),
96
+ setVisitContext: jest.fn(),
97
+ });
77
98
  });
78
99
 
79
100
  it('shows empty state when no immunizations are recorded', async () => {
@@ -164,10 +185,19 @@ describe('ImmunizationsDetailedSummary', () => {
164
185
  });
165
186
 
166
187
  it('opens immunization form when add button is clicked during an active visit', async () => {
188
+ mockUsePatientChartStore.mockReturnValue({
189
+ patientUuid: 'patient-123',
190
+ patient: null,
191
+ visitContext: mockCurrentVisit,
192
+ mutateVisitContext: jest.fn(),
193
+ setPatient: jest.fn(),
194
+ setVisitContext: jest.fn(),
195
+ });
196
+
167
197
  const user = userEvent.setup();
168
198
  const mockLaunchStartVisitPrompt = jest.fn();
169
199
  mockUseVisit.mockReturnValue({ currentVisit: { uuid: 'visit-uuid' } } as VisitReturnType);
170
- mockUseImmunizations.mockReturnValueOnce({
200
+ mockUseImmunizations.mockReturnValue({
171
201
  data: mockImmunizationData,
172
202
  isLoading: false,
173
203
  error: null,
@@ -191,7 +221,14 @@ describe('ImmunizationsDetailedSummary', () => {
191
221
  it('prompts to start visit when add button is clicked without an active visit', async () => {
192
222
  const user = userEvent.setup();
193
223
  const mockLaunchStartVisitPrompt = jest.fn();
194
- mockUseVisit.mockReturnValue({ currentVisit: null } as VisitReturnType);
224
+ mockUsePatientChartStore.mockReturnValue({
225
+ patientUuid: mockPatient.id,
226
+ patient: mockPatient,
227
+ visitContext: null,
228
+ mutateVisitContext: null,
229
+ setPatient: jest.fn(),
230
+ setVisitContext: jest.fn(),
231
+ });
195
232
  mockUseImmunizations.mockReturnValueOnce({
196
233
  data: mockImmunizationData,
197
234
  isLoading: false,
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import dayjs from 'dayjs';
3
3
  import userEvent from '@testing-library/user-event';
4
- import { render, screen } from '@testing-library/react';
4
+ import { render, screen, waitFor } from '@testing-library/react';
5
5
  import {
6
6
  getDefaultsFromConfigSchema,
7
7
  showSnackbar,
@@ -9,8 +9,8 @@ import {
9
9
  toOmrsIsoString,
10
10
  useConfig,
11
11
  useSession,
12
- useVisit,
13
12
  } from '@openmrs/esm-framework';
13
+ import { type PatientWorkspace2DefinitionProps } from '@openmrs/esm-patient-common-lib';
14
14
  import { configSchema, type ImmunizationConfigObject } from '../config-schema';
15
15
  import { immunizationFormSub } from './utils';
16
16
  import { mockCurrentVisit, mockSessionDataResponse } from '__mocks__';
@@ -26,8 +26,6 @@ const mockSavePatientImmunization = savePatientImmunization as jest.Mock;
26
26
  const mockSetTitle = jest.fn();
27
27
  const mockUseConfig = jest.mocked<() => ImmunizationConfigObject>(useConfig);
28
28
  const mockUseSession = jest.mocked(useSession);
29
- const mockUseVisit = jest.mocked(useVisit);
30
- const mockMutate = jest.fn();
31
29
  const mockToOmrsIsoString = jest.mocked(toOmrsIsoString);
32
30
  const mockToDateObjectStrict = jest.mocked(toDateObjectStrict);
33
31
 
@@ -67,13 +65,20 @@ jest.mock('./immunizations.resource', () => ({
67
65
  savePatientImmunization: jest.fn(),
68
66
  }));
69
67
 
70
- const testProps = {
71
- patientUuid: mockPatient.id,
72
- patient: mockPatient,
68
+ const testProps: PatientWorkspace2DefinitionProps<{}, {}> = {
73
69
  closeWorkspace: mockCloseWorkspace,
74
- closeWorkspaceWithSavedChanges: mockCloseWorkspaceWithSavedChanges,
75
- promptBeforeClosing: mockPromptBeforeClosing,
76
- setTitle: mockSetTitle,
70
+ groupProps: {
71
+ patientUuid: mockPatient.id,
72
+ patient: mockPatient,
73
+ visitContext: mockCurrentVisit,
74
+ mutateVisitContext: null,
75
+ },
76
+ workspaceName: '',
77
+ launchChildWorkspace: jest.fn(),
78
+ workspaceProps: {},
79
+ windowProps: {},
80
+ windowName: '',
81
+ isRootWorkspace: false,
77
82
  };
78
83
 
79
84
  mockUseConfig.mockReturnValue({
@@ -95,15 +100,6 @@ mockUseConfig.mockReturnValue({
95
100
  });
96
101
 
97
102
  mockUseSession.mockReturnValue(mockSessionDataResponse.data);
98
- mockUseVisit.mockReturnValue({
99
- activeVisit: mockCurrentVisit,
100
- currentVisit: mockCurrentVisit,
101
- currentVisitIsRetrospective: false,
102
- error: null,
103
- isLoading: false,
104
- isValidating: false,
105
- mutate: mockMutate,
106
- });
107
103
 
108
104
  describe('Immunizations Form', () => {
109
105
  const isoFormat = 'YYYY-MM-DDTHH:mm:ss.SSSZZ';
@@ -114,10 +110,11 @@ describe('Immunizations Form', () => {
114
110
  mockToDateObjectStrict.mockImplementation((dateString) => dayjs(dateString, isoFormat).toDate());
115
111
  });
116
112
 
117
- it('should render ImmunizationsForm component', () => {
113
+ it('should render ImmunizationsForm component', async () => {
118
114
  render(<ImmunizationsForm {...testProps} />);
119
115
 
120
- expect(screen.getByLabelText(/vaccination date/i)).toBeInTheDocument();
116
+ await screen.findByLabelText(/vaccination date/i);
117
+
121
118
  expect(screen.getByRole('combobox', { name: /Immunization/i })).toBeInTheDocument();
122
119
  expect(screen.getByRole('textbox', { name: /note/i })).toBeInTheDocument();
123
120
  expect(screen.getByText(/Vaccine Batch Information/i)).toBeInTheDocument();
@@ -230,10 +227,10 @@ describe('Immunizations Form', () => {
230
227
  const immunizationToEdit = {
231
228
  vaccineUuid: '886AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
232
229
  immunizationId: '0a6ca2bb-a317-49d8-bd6b-dabb658840d2',
233
- vaccinationDate: new Date('2024-01-03').toString(),
230
+ vaccinationDate: new Date('2024-01-03').toISOString(),
234
231
  doseNumber: 2,
235
- expirationDate: new Date('2024-05-19').toString(),
236
- nextDoseDate: new Date('2024-01-03').toString(),
232
+ expirationDate: new Date('2024-05-19').toISOString(),
233
+ nextDoseDate: new Date('2024-01-03').toISOString(),
237
234
  note: 'Given as part of routine schedule.',
238
235
  lotNumber: 'A123456',
239
236
  manufacturer: 'Merck & Co., Inc.',
@@ -283,7 +280,7 @@ describe('Immunizations Form', () => {
283
280
  expect.objectContaining({
284
281
  encounter: { reference: 'Encounter/ce589c9c-2f30-42ec-b289-a153f812ea5e', type: 'Encounter' },
285
282
  id: '0a6ca2bb-a317-49d8-bd6b-dabb658840d2',
286
- expirationDate: dayjs(new Date('2024-05-19')).startOf('day').toDate().toISOString(),
283
+ expirationDate: '2024-05-19',
287
284
  extension: [
288
285
  {
289
286
  url: FHIR_NEXT_DOSE_DATE_EXTENSION_URL,
@@ -316,6 +313,150 @@ describe('Immunizations Form', () => {
316
313
  title: 'Vaccination saved successfully',
317
314
  });
318
315
  });
316
+
317
+ it('should save new immunization with expiration date in correct format', async () => {
318
+ const user = userEvent.setup();
319
+
320
+ // Pre-populate form with expiration date using the form subscription (same pattern as edit tests)
321
+ const immunizationWithExpiration = {
322
+ vaccineUuid: '782AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
323
+ vaccinationDate: new Date('2024-06-15').toISOString(),
324
+ doseNumber: 1,
325
+ expirationDate: new Date('2025-12-31').toISOString(),
326
+ manufacturer: 'Pfizer',
327
+ lotNumber: 'LOT123',
328
+ note: '',
329
+ nextDoseDate: null,
330
+ };
331
+
332
+ immunizationFormSub.next(immunizationWithExpiration);
333
+
334
+ mockSavePatientImmunization.mockResolvedValue({
335
+ status: 201,
336
+ ok: true,
337
+ data: {
338
+ id: 'new-immunization-id',
339
+ },
340
+ });
341
+
342
+ render(<ImmunizationsForm {...testProps} />);
343
+
344
+ // Verify the form is populated
345
+ const expirationDateField = screen.getByRole('textbox', { name: /Expiration date/i });
346
+ expect(expirationDateField).toHaveValue('31/12/2025');
347
+
348
+ // Submit without making changes
349
+ const saveButton = screen.getByRole('button', { name: /Save/i });
350
+ await user.click(saveButton);
351
+
352
+ // Verify that expirationDate is formatted as YYYY-MM-DD without timezone
353
+ expect(mockSavePatientImmunization).toHaveBeenCalledWith(
354
+ expect.objectContaining({
355
+ expirationDate: '2025-12-31', // Date-only format, not ISO string with time/timezone
356
+ lotNumber: 'LOT123',
357
+ manufacturer: { display: 'Pfizer' },
358
+ }),
359
+ undefined,
360
+ expect.any(AbortController),
361
+ );
362
+ });
363
+
364
+ it('should format expiration date as date-only string without timezone', async () => {
365
+ const user = userEvent.setup();
366
+
367
+ // Regression test for O3-4970:
368
+ // Previously, expiration dates were converted to ISO strings with timezone (e.g., "2025-12-31T00:00:00.000Z"),
369
+ // causing a one-day shift for users in timezones ahead of UTC. This test ensures dates are sent as
370
+ // date-only strings (e.g., "2025-12-31") per FHIR date type specification, preventing timezone conversion.
371
+
372
+ // Setup immunization with expiration date
373
+ const immunizationWithExpiration = {
374
+ vaccineUuid: '782AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
375
+ immunizationId: 'test-immunization-with-expiration',
376
+ vaccinationDate: new Date('2024-12-25').toISOString(),
377
+ doseNumber: 1,
378
+ expirationDate: new Date('2025-12-31').toISOString(),
379
+ manufacturer: 'Test Manufacturer',
380
+ lotNumber: 'LOT123',
381
+ note: 'Test note',
382
+ nextDoseDate: null,
383
+ };
384
+
385
+ immunizationFormSub.next(immunizationWithExpiration);
386
+
387
+ mockSavePatientImmunization.mockResolvedValue({
388
+ status: 201,
389
+ ok: true,
390
+ data: {
391
+ id: immunizationWithExpiration.immunizationId,
392
+ },
393
+ });
394
+
395
+ render(<ImmunizationsForm {...testProps} />);
396
+
397
+ // Verify the expiration date is displayed correctly
398
+ const expirationDateField = screen.getByRole('textbox', { name: /Expiration date/i });
399
+ expect(expirationDateField).toHaveValue('31/12/2025');
400
+
401
+ // Submit the form without changes to verify the date format is preserved
402
+ const saveButton = screen.getByRole('button', { name: /Save/i });
403
+ await user.click(saveButton);
404
+
405
+ // Verify that expirationDate is formatted as YYYY-MM-DD without timezone (not ISO string)
406
+ expect(mockSavePatientImmunization).toHaveBeenCalledWith(
407
+ expect.objectContaining({
408
+ expirationDate: '2025-12-31', // Date-only format, not ISO string with time/timezone
409
+ }),
410
+ immunizationWithExpiration.immunizationId,
411
+ expect.any(AbortController),
412
+ );
413
+ });
414
+
415
+ it('should preserve date format when submitting immunization with different expiration date', async () => {
416
+ const user = userEvent.setup();
417
+
418
+ // Load existing immunization with a different expiration date
419
+ const immunizationToEdit = {
420
+ vaccineUuid: '782AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
421
+ immunizationId: 'existing-immunization-id',
422
+ vaccinationDate: new Date('2024-06-15').toISOString(),
423
+ doseNumber: 1,
424
+ expirationDate: new Date('2026-06-15').toISOString(),
425
+ manufacturer: 'Moderna',
426
+ lotNumber: 'ABC123',
427
+ note: 'Initial note',
428
+ nextDoseDate: null,
429
+ };
430
+
431
+ immunizationFormSub.next(immunizationToEdit);
432
+
433
+ mockSavePatientImmunization.mockResolvedValue({
434
+ status: 201,
435
+ ok: true,
436
+ data: {
437
+ id: immunizationToEdit.immunizationId,
438
+ },
439
+ });
440
+
441
+ render(<ImmunizationsForm {...testProps} />);
442
+
443
+ // Verify expiration date is displayed
444
+ const expirationDateField = screen.getByRole('textbox', { name: /Expiration date/i });
445
+ expect(expirationDateField).toHaveValue('15/06/2026');
446
+
447
+ // Submit the form
448
+ const saveButton = screen.getByRole('button', { name: /Save/i });
449
+ await user.click(saveButton);
450
+
451
+ // Verify the date is sent in correct format (YYYY-MM-DD, not ISO string)
452
+ expect(mockSavePatientImmunization).toHaveBeenCalledWith(
453
+ expect.objectContaining({
454
+ expirationDate: '2026-06-15', // Date-only format, not ISO string with time/timezone
455
+ }),
456
+ immunizationToEdit.immunizationId,
457
+ expect.any(AbortController),
458
+ );
459
+ });
319
460
  });
320
461
 
321
462
  async function selectOption(dropdown: HTMLElement, optionLabel: string) {