@openmrs/esm-stock-management-app 1.0.1-pre.777 → 1.0.1-pre.785

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 (120) hide show
  1. package/__mocks__/index.ts +1 -0
  2. package/__mocks__/operation-type.mock.ts +532 -0
  3. package/dist/155.js +1 -0
  4. package/dist/155.js.map +1 -0
  5. package/dist/172.js +1 -1
  6. package/dist/20.js +1 -1
  7. package/dist/290.js +1 -1
  8. package/dist/493.js +2 -0
  9. package/dist/493.js.map +1 -0
  10. package/dist/606.js +1 -1
  11. package/dist/627.js +1 -1
  12. package/dist/{400.js → 914.js} +1 -1
  13. package/dist/914.js.map +1 -0
  14. package/dist/main.js +1 -1
  15. package/dist/main.js.map +1 -1
  16. package/dist/openmrs-esm-stock-management-app.js +1 -1
  17. package/dist/openmrs-esm-stock-management-app.js.buildmanifest.json +81 -57
  18. package/dist/openmrs-esm-stock-management-app.js.map +1 -1
  19. package/dist/routes.json +1 -1
  20. package/package.json +1 -1
  21. package/src/core/utils/utils.ts +29 -0
  22. package/src/index.ts +4 -0
  23. package/src/routes.json +9 -0
  24. package/src/stock-items/add-stock-item/transactions/printout/transactions-stockcard-printout.component.tsx +8 -12
  25. package/src/stock-items/add-stock-item/transactions/transactions.component.tsx +8 -12
  26. package/src/stock-items/stock-items.resource.ts +5 -5
  27. package/src/stock-lookups/stock-lookups.resource.ts +2 -2
  28. package/src/stock-operations/edit-stock-operation/edit-stock-operation-action-menu.component.tsx +41 -16
  29. package/src/stock-operations/{add-stock-operation/received-items.component.tsx → received-items.component.tsx} +1 -1
  30. package/src/stock-operations/stock-operation-reference.component.tsx +64 -0
  31. package/src/stock-operations/stock-operation-status/stock-operation-status-row.tsx +77 -0
  32. package/src/stock-operations/stock-operation-status/stock-operation-status.scss +32 -0
  33. package/src/stock-operations/stock-operation-status/stock-operation-status.tsx +45 -0
  34. package/src/stock-operations/stock-operation-types-selector/stock-operation-types-selector.component.tsx +30 -29
  35. package/src/stock-operations/stock-operation.utils.tsx +16 -79
  36. package/src/stock-operations/stock-operations-dialog/stock-operations-issue-stock-button.component.tsx +27 -39
  37. package/src/stock-operations/stock-operations-dialog/stock-operations-print-button.component.tsx +51 -59
  38. package/src/stock-operations/{stock-item-selector/stock-item-selector.resource.tsx → stock-operations-forms/hooks/useFilterableStockItems.ts} +4 -4
  39. package/src/stock-operations/stock-operations-forms/hooks/useFilteredOperationTypesByRoles.ts +30 -0
  40. package/src/stock-operations/stock-operations-forms/hooks/useOperationTypePermisions.ts +29 -0
  41. package/src/stock-operations/stock-operations-forms/hooks/useParties.ts +73 -0
  42. package/src/stock-operations/{users-selector/users-selector.resource.tsx → stock-operations-forms/hooks/useSearchUser.ts} +9 -7
  43. package/src/stock-operations/{batch-no-selector/batch-no-selector.resource.tsx → stock-operations-forms/hooks/useStockItemBatchNumbers.ts} +3 -3
  44. package/src/stock-operations/stock-operations-forms/hooks/useStockOperationLinks.ts +20 -0
  45. package/src/stock-operations/stock-operations-forms/input-components/batch-no-selector.component.tsx +72 -0
  46. package/src/stock-operations/stock-operations-forms/input-components/batch-no-selector.test.tsx +90 -0
  47. package/src/stock-operations/{add-stock-operation/stock-item-search/stock-item-search.scss → stock-operations-forms/input-components/input-components-styles.scss} +2 -2
  48. package/src/stock-operations/stock-operations-forms/input-components/qty-uim-selector.test.tsx +157 -0
  49. package/src/stock-operations/stock-operations-forms/input-components/quantity-uom-selector.component.tsx +53 -0
  50. package/src/stock-operations/stock-operations-forms/input-components/stock-item-search.component.tsx +56 -0
  51. package/src/stock-operations/stock-operations-forms/input-components/stock-operation-reason-selector.component.tsx +59 -0
  52. package/src/stock-operations/stock-operations-forms/input-components/stock-operation-reason-selector.test.tsx +216 -0
  53. package/src/stock-operations/stock-operations-forms/input-components/unique-batch-no-entry-input.component.tsx +59 -0
  54. package/src/stock-operations/stock-operations-forms/input-components/user-selector.test.tsx +110 -0
  55. package/src/stock-operations/stock-operations-forms/input-components/users-selector.component.tsx +111 -0
  56. package/src/stock-operations/stock-operations-forms/step1.test.tsx +303 -0
  57. package/src/stock-operations/stock-operations-forms/step2.test.tsx +250 -0
  58. package/src/stock-operations/stock-operations-forms/step3.test.tsx +223 -0
  59. package/src/stock-operations/stock-operations-forms/steps/base-operation-details-form-step.tsx +241 -0
  60. package/src/stock-operations/stock-operations-forms/steps/quantity-uom-cell.component.tsx +33 -0
  61. package/src/stock-operations/stock-operations-forms/steps/stock-availability-cell.component.tsx +51 -0
  62. package/src/stock-operations/stock-operations-forms/steps/stock-operation-item-batch-no-cell.component.tsx +40 -0
  63. package/src/stock-operations/stock-operations-forms/steps/stock-operation-item-cell.component.tsx +38 -0
  64. package/src/stock-operations/stock-operations-forms/steps/stock-operation-item-expiry-cell.component.tsx +41 -0
  65. package/src/stock-operations/stock-operations-forms/steps/stock-operation-items-form-step.component.tsx +281 -0
  66. package/src/stock-operations/stock-operations-forms/steps/stock-operation-items-form-step.scc.scss +64 -0
  67. package/src/stock-operations/stock-operations-forms/steps/stock-operation-submission-form-step.component.tsx +236 -0
  68. package/src/stock-operations/stock-operations-forms/stock-issue-form-initializer-with-related-requisition-operation.component.tsx +55 -0
  69. package/src/stock-operations/stock-operations-forms/stock-item-form/stock-item-form.scss +41 -0
  70. package/src/stock-operations/stock-operations-forms/stock-item-form/stock-item-form.workspace.tsx +197 -0
  71. package/src/stock-operations/stock-operations-forms/stock-operation-form-header.component.tsx +166 -0
  72. package/src/stock-operations/stock-operations-forms/stock-operation-form.component.tsx +200 -0
  73. package/src/stock-operations/stock-operations-forms/stock-operation-form.scss +111 -0
  74. package/src/stock-operations/stock-operations-forms/stock-operation-related-link.component.tsx +45 -0
  75. package/src/stock-operations/stock-operations-forms/stock-operation-stepper/stepper.scss +41 -0
  76. package/src/stock-operations/stock-operations-forms/stock-operation-stepper/stock-operation-stepper.component.tsx +52 -0
  77. package/src/stock-operations/stock-operations-forms/stock-operations-form-utils.ts +32 -0
  78. package/src/stock-operations/stock-operations-table.component.tsx +20 -56
  79. package/src/stock-operations/stock-operations.resource.ts +16 -13
  80. package/src/stock-operations/validation-schema.ts +72 -14
  81. package/dist/400.js.map +0 -1
  82. package/dist/766.js +0 -2
  83. package/dist/766.js.map +0 -1
  84. package/src/stock-operations/add-stock-operation/add-stock-operation.component.tsx +0 -349
  85. package/src/stock-operations/add-stock-operation/add-stock-operation.resource.tsx +0 -27
  86. package/src/stock-operations/add-stock-operation/add-stock-operation.scss +0 -60
  87. package/src/stock-operations/add-stock-operation/add-stock-operation.test.tsx +0 -192
  88. package/src/stock-operations/add-stock-operation/add-stock-operation.utils.tsx +0 -152
  89. package/src/stock-operations/add-stock-operation/add-stock-utils.ts +0 -103
  90. package/src/stock-operations/add-stock-operation/base-operation-details.component.tsx +0 -439
  91. package/src/stock-operations/add-stock-operation/base-operation-details.scss +0 -30
  92. package/src/stock-operations/add-stock-operation/stock-item-search/stock-item-search.component.tsx +0 -70
  93. package/src/stock-operations/add-stock-operation/stock-items-addition-row.component.tsx +0 -360
  94. package/src/stock-operations/add-stock-operation/stock-items-addition-row.resource.tsx +0 -0
  95. package/src/stock-operations/add-stock-operation/stock-items-addition-row.scss +0 -12
  96. package/src/stock-operations/add-stock-operation/stock-items-addition-row.test.tsx +0 -10
  97. package/src/stock-operations/add-stock-operation/stock-items-addition.component.scss +0 -17
  98. package/src/stock-operations/add-stock-operation/stock-items-addition.component.tsx +0 -254
  99. package/src/stock-operations/add-stock-operation/stock-operation-context/useStockOperationContext.tsx +0 -16
  100. package/src/stock-operations/add-stock-operation/stock-operation-reference.component.tsx +0 -39
  101. package/src/stock-operations/add-stock-operation/stock-operation-related-link.component.tsx +0 -38
  102. package/src/stock-operations/add-stock-operation/stock-operation-status.component.tsx +0 -170
  103. package/src/stock-operations/add-stock-operation/stock-operation-submission.component.tsx +0 -189
  104. package/src/stock-operations/add-stock-operation/stock-operation-submission.test.tsx +0 -138
  105. package/src/stock-operations/add-stock-operation/types.ts +0 -55
  106. package/src/stock-operations/add-stock-operation/validationSchema.ts +0 -54
  107. package/src/stock-operations/batch-no-selector/batch-no-selector.component.tsx +0 -114
  108. package/src/stock-operations/batch-no-selector/batch-no-selector.scss +0 -0
  109. package/src/stock-operations/batch-no-selector/batch-no-selector.test.tsx +0 -101
  110. package/src/stock-operations/party-selector/party-selector.component.tsx +0 -59
  111. package/src/stock-operations/qty-uom-selector/qty-uom-selector.component.tsx +0 -65
  112. package/src/stock-operations/qty-uom-selector/qty-uom-selector.resource.tsx +0 -0
  113. package/src/stock-operations/qty-uom-selector/qty-uom-selector.scss +0 -0
  114. package/src/stock-operations/qty-uom-selector/qty-uom-selector.test.tsx +0 -10
  115. package/src/stock-operations/stock-item-selector/stock-item-selector.component.tsx +0 -69
  116. package/src/stock-operations/stock-item-selector/stock-item-selector.scss +0 -0
  117. package/src/stock-operations/stock-item-selector/stock-item-selector.test.tsx +0 -10
  118. package/src/stock-operations/stock-operation-reason-selector/stock-operation-reason-selector.component.tsx +0 -62
  119. package/src/stock-operations/users-selector/users-selector.component.tsx +0 -75
  120. /package/dist/{766.js.LICENSE.txt → 493.js.LICENSE.txt} +0 -0
@@ -0,0 +1,157 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import React from 'react';
4
+ import { useStockItem } from '../../../stock-items/stock-items.resource';
5
+ import QtyUomSelector from './quantity-uom-selector.component';
6
+
7
+ jest.mock('../../../stock-items/stock-items.resource');
8
+ jest.mock('react-i18next', () => ({
9
+ useTranslation: () => ({ t: (key: string) => key }),
10
+ }));
11
+
12
+ const mockUseStockItem = useStockItem as jest.Mock;
13
+
14
+ describe('QtyUOMSelector', () => {
15
+ const mockOnValueChange = jest.fn();
16
+ const mockStockItemUuid = 'test-uuid';
17
+ const mockitem = {
18
+ uuid: '33225466-93c8-4720-b110-4f445f3764c6',
19
+ drugUuid: '1776AFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
20
+ drugName: 'Ibuprofen Injection solution 5mg/mL ',
21
+ conceptUuid: '77897AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
22
+ conceptName: 'IBUPROFEN',
23
+ hasExpiration: true,
24
+ preferredVendorUuid: 'b7e481ef-06e7-46ba-8a7c-ab077aa3355f',
25
+ preferredVendorName: 'Kenya Medical Supplies Authority',
26
+ purchasePrice: 100.0,
27
+ purchasePriceUoMUuid: '65dc9ec7-0663-4cf0-a35c-6a2d2cf99eee',
28
+ purchasePriceUoMName: 'Box',
29
+ purchasePriceUoMFactor: 10.0,
30
+ dispensingUnitName: 'Tablet',
31
+ dispensingUnitUuid: '1513AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
32
+ dispensingUnitPackagingUoMUuid: '65dc9ec7-0663-4cf0-a35c-6a2d2cf99eee',
33
+ dispensingUnitPackagingUoMName: 'Box',
34
+ dispensingUnitPackagingUoMFactor: 10.0,
35
+ defaultStockOperationsUoMUuid: null,
36
+ defaultStockOperationsUoMName: null,
37
+ defaultStockOperationsUoMFactor: null,
38
+ categoryUuid: '183f9497-b72e-4ebe-ae17-5dcf110ff3b6',
39
+ categoryName: 'Other Lymph Node Comments',
40
+ commonName: 'Ibuprofen 5 MG/ML Injectable Solution',
41
+ acronym: null,
42
+ reorderLevel: null,
43
+ reorderLevelUoMUuid: null,
44
+ reorderLevelUoMName: null,
45
+ reorderLevelUoMFactor: null,
46
+ dateCreated: '2024-09-03T09:46:47.000+0300',
47
+ creatorGivenName: 'Mark',
48
+ creatorFamilyName: 'Miller',
49
+ voided: false,
50
+ expiryNotice: 180,
51
+ permission: {
52
+ canView: true,
53
+ canEdit: true,
54
+ },
55
+ packagingUnits: [
56
+ {
57
+ uuid: '8023ef7d-56da-442f-bc74-319f000840b4',
58
+ factor: 1.0,
59
+ packagingUomUuid: '162382AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
60
+ packagingUomName: 'Vial',
61
+ stockItemUuid: '33225466-93c8-4720-b110-4f445f3764c6',
62
+ isDefaultStockOperationsUoM: false,
63
+ isDispensingUnit: false,
64
+ links: [
65
+ {
66
+ rel: 'full',
67
+ uri: 'http://hie.kenyahmis.org/openmrs/ws/rest/v1/stockmanagement/stockitempackaginguom/8023ef7d-56da-442f-bc74-319f000840b4?v=full',
68
+ resourceAlias: 'stockitempackaginguom',
69
+ },
70
+ ],
71
+ resourceVersion: '1.8',
72
+ },
73
+ {
74
+ uuid: '65dc9ec7-0663-4cf0-a35c-6a2d2cf99eee',
75
+ factor: 10.0,
76
+ packagingUomUuid: '162396AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
77
+ packagingUomName: 'Box',
78
+ stockItemUuid: '33225466-93c8-4720-b110-4f445f3764c6',
79
+ isDefaultStockOperationsUoM: false,
80
+ isDispensingUnit: true,
81
+ links: [
82
+ {
83
+ rel: 'full',
84
+ uri: 'http://hie.kenyahmis.org/openmrs/ws/rest/v1/stockmanagement/stockitempackaginguom/65dc9ec7-0663-4cf0-a35c-6a2d2cf99eee?v=full',
85
+ resourceAlias: 'stockitempackaginguom',
86
+ },
87
+ ],
88
+ resourceVersion: '1.8',
89
+ },
90
+ ],
91
+ references: [],
92
+ links: [
93
+ {
94
+ rel: 'self',
95
+ uri: 'http://hie.kenyahmis.org/openmrs/ws/rest/v1/stockmanagement/stockitem/33225466-93c8-4720-b110-4f445f3764c6',
96
+ resourceAlias: 'stockitem',
97
+ },
98
+ ],
99
+ resourceVersion: '1.8',
100
+ };
101
+
102
+ beforeEach(() => {
103
+ jest.clearAllMocks();
104
+ mockUseStockItem.mockReturnValue({
105
+ isLoading: false,
106
+ item: mockitem,
107
+ });
108
+ });
109
+ it('should render Skeleton when loading stock item', () => {
110
+ mockUseStockItem.mockReturnValue({ isLoading: true });
111
+ render(<QtyUomSelector stockItemUuid={mockStockItemUuid} onValueChange={mockOnValueChange} />);
112
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
113
+ });
114
+ it('should render Inline notification error when error ocuured while fetching item', () => {
115
+ const errorMessage = 'error loading stock item';
116
+ mockUseStockItem.mockReturnValue({ isLoading: false, error: new Error(errorMessage) });
117
+ render(<QtyUomSelector stockItemUuid={mockStockItemUuid} onValueChange={mockOnValueChange} />);
118
+ expect(screen.getByText(errorMessage)).toBeInTheDocument();
119
+ expect(screen.getByRole('status')).toBeInTheDocument();
120
+ });
121
+ it('should render Inline notification error when error ocuured while fetching item', () => {
122
+ const errorMessage = 'error loading stock item';
123
+ mockUseStockItem.mockReturnValue({ isLoading: false, error: new Error(errorMessage) });
124
+ render(<QtyUomSelector stockItemUuid={mockStockItemUuid} onValueChange={mockOnValueChange} />);
125
+ expect(screen.getByText(errorMessage)).toBeInTheDocument();
126
+ expect(screen.getByRole('status')).toBeInTheDocument();
127
+ });
128
+
129
+ it('should display error message when error prop is provided', () => {
130
+ const errorMessage = 'This is an error';
131
+ render(<QtyUomSelector stockItemUuid={mockStockItemUuid} onValueChange={mockOnValueChange} error={errorMessage} />);
132
+ expect(screen.getByText(errorMessage)).toBeInTheDocument();
133
+ });
134
+ it('should display error message when error prop is provided', () => {
135
+ const errorMessage = 'This is an error';
136
+ render(<QtyUomSelector stockItemUuid={mockStockItemUuid} onValueChange={mockOnValueChange} error={errorMessage} />);
137
+ expect(screen.getByText(errorMessage)).toBeInTheDocument();
138
+ });
139
+
140
+ it('should render packing uom options', async () => {
141
+ render(<QtyUomSelector stockItemUuid={mockStockItemUuid} onValueChange={mockOnValueChange} />);
142
+ const combobox = screen.getByRole('combobox');
143
+ await userEvent.click(combobox);
144
+ mockitem.packagingUnits.forEach(({ factor, packagingUomName }) => {
145
+ expect(screen.getByText(`${packagingUomName} - ${factor}`)).toBeInTheDocument();
146
+ });
147
+ });
148
+ it('should packing uom selection', async () => {
149
+ const itemToSelect = mockitem.packagingUnits[1];
150
+ render(<QtyUomSelector stockItemUuid={mockStockItemUuid} onValueChange={mockOnValueChange} />);
151
+ const combobox = screen.getByRole('combobox');
152
+ await userEvent.click(combobox);
153
+ const option = screen.getByText(`${itemToSelect.packagingUomName} - ${itemToSelect.factor}`);
154
+ await userEvent.click(option);
155
+ expect(mockOnValueChange).toHaveBeenCalledWith(itemToSelect.uuid);
156
+ });
157
+ });
@@ -0,0 +1,53 @@
1
+ import { ComboBox, InlineNotification, SkeletonText } from '@carbon/react';
2
+ import React, { useMemo } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { StockItemPackagingUOMDTO } from '../../../core/api/types/stockItem/StockItemPackagingUOM';
5
+ import { useStockItem } from '../../../stock-items/stock-items.resource';
6
+
7
+ interface QtyUomSelectorProps {
8
+ stockItemUuid: string;
9
+ intiallvalue?: string;
10
+ onValueChange?: (value: string) => void;
11
+ error?: string;
12
+ }
13
+
14
+ const QtyUomSelector: React.FC<QtyUomSelectorProps> = ({ stockItemUuid, error, intiallvalue, onValueChange }) => {
15
+ const { t } = useTranslation();
16
+ const { isLoading, error: stockItemError, item } = useStockItem(stockItemUuid);
17
+ const initialSelectedItem = useMemo<StockItemPackagingUOMDTO | null>(
18
+ () => (item?.packagingUnits ?? []).find((u) => u.uuid === intiallvalue),
19
+ [item?.packagingUnits, intiallvalue],
20
+ );
21
+
22
+ if (isLoading) return <SkeletonText role="progressbar" />;
23
+
24
+ if (stockItemError)
25
+ return (
26
+ <InlineNotification
27
+ kind="error"
28
+ title={t('packagingUomError', 'Error loading Stock item')}
29
+ subtitle={stockItemError?.message}
30
+ />
31
+ );
32
+
33
+ return (
34
+ <ComboBox
35
+ titleText={t('quantityUom', 'Qty UoM')}
36
+ name={'stockItemPackagingUOMUuid'}
37
+ id={'stockItemPackagingUOMUuid'}
38
+ items={item?.packagingUnits ?? []}
39
+ onChange={(data: { selectedItem?: StockItemPackagingUOMDTO }) => {
40
+ onValueChange?.(data.selectedItem?.uuid);
41
+ }}
42
+ initialSelectedItem={initialSelectedItem}
43
+ itemToString={(s: StockItemPackagingUOMDTO) =>
44
+ s.packagingUomName ? `${s?.packagingUomName} - ${s?.factor} ` : ''
45
+ }
46
+ placeholder={t('filter', 'Filter') + '...'}
47
+ invalid={error}
48
+ invalidText={error}
49
+ />
50
+ );
51
+ };
52
+
53
+ export default QtyUomSelector;
@@ -0,0 +1,56 @@
1
+ import { ClickableTile, Search } from '@carbon/react';
2
+ import { useDebounce } from '@openmrs/esm-framework';
3
+ import React, { useEffect, useState } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { StockItemDTO } from '../../../core/api/types/stockItem/StockItem';
6
+ import { useFilterableStockItems } from '../hooks/useFilterableStockItems';
7
+ import styles from './input-components-styles.scss';
8
+
9
+ type StockItemSearchProps = {
10
+ onSelectedItem?: (stockItem: StockItemDTO) => void;
11
+ };
12
+
13
+ const StockItemSearch: React.FC<StockItemSearchProps> = ({ onSelectedItem }) => {
14
+ const { t } = useTranslation();
15
+ const { isLoading, stockItemsList, setSearchString } = useFilterableStockItems({});
16
+ const [searchTerm, setSearchTerm] = useState('');
17
+ const debouncedSearchTerm = useDebounce(searchTerm);
18
+
19
+ useEffect(() => {
20
+ if (debouncedSearchTerm?.length !== 0) {
21
+ setSearchString(debouncedSearchTerm);
22
+ }
23
+ }, [debouncedSearchTerm, setSearchString]);
24
+
25
+ const handleOnSearchResultClick = (stockItem: StockItemDTO) => {
26
+ onSelectedItem?.(stockItem);
27
+ setSearchTerm('');
28
+ };
29
+ return (
30
+ <div className={styles.stockItemSearchContainer}>
31
+ <div style={{ display: 'flex' }}>
32
+ <Search
33
+ size="lg"
34
+ placeholder={t('findItems', 'Find your items')}
35
+ labelText={t('search', 'Search')}
36
+ closeButtonLabelText={t('clearSearch', 'Clear search input')}
37
+ value={searchTerm}
38
+ id="search-stock-operation-item"
39
+ name="search-stock-operation-item"
40
+ onChange={(e) => setSearchTerm(e.target.value)}
41
+ />
42
+ </div>
43
+ {searchTerm && stockItemsList?.length > 0 && (
44
+ <div className={styles.searchResults}>
45
+ {stockItemsList?.slice(0, 5).map((stockItem) => (
46
+ <ClickableTile onClick={() => handleOnSearchResultClick(stockItem)} key={stockItem?.uuid}>
47
+ {stockItem?.commonName}
48
+ </ClickableTile>
49
+ ))}
50
+ </div>
51
+ )}
52
+ </div>
53
+ );
54
+ };
55
+
56
+ export default StockItemSearch;
@@ -0,0 +1,59 @@
1
+ import { ComboBox, InlineNotification, SelectSkeleton } from '@carbon/react';
2
+ import { useConfig } from '@openmrs/esm-framework';
3
+ import React from 'react';
4
+ import { Controller, useFormContext } from 'react-hook-form';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { ConfigObject } from '../../../config-schema';
7
+ import { Concept } from '../../../core/api/types/concept/Concept';
8
+ import { useConcept } from '../../../stock-lookups/stock-lookups.resource';
9
+
10
+ const StockOperationReasonSelector = () => {
11
+ const { stockAdjustmentReasonUUID } = useConfig<ConfigObject>();
12
+ const form = useFormContext<{ reasonUuid: string }>();
13
+ const {
14
+ isLoading,
15
+ error,
16
+ items: { answers: reasons },
17
+ } = useConcept(stockAdjustmentReasonUUID);
18
+ const { t } = useTranslation();
19
+ if (isLoading) return <SelectSkeleton role="progressbar" />;
20
+
21
+ if (error)
22
+ return (
23
+ <InlineNotification
24
+ lowContrast
25
+ kind="error"
26
+ title={t('reasonError', 'Error loading reasons concepts')}
27
+ subtitle={error?.message}
28
+ />
29
+ );
30
+
31
+ return (
32
+ <Controller
33
+ control={form.control}
34
+ name={'reasonUuid'}
35
+ render={({ field, fieldState: { error } }) => (
36
+ <ComboBox
37
+ readOnly={field.disabled}
38
+ titleText={t('reason', 'Reason')}
39
+ placeholder={t('chooseAReason', 'Choose a reason')}
40
+ name={'reasonUuid'}
41
+ id={'reasonUuid'}
42
+ size={'xl'}
43
+ items={reasons}
44
+ initialSelectedItem={reasons?.find((p) => p.uuid === field.value)}
45
+ selectedItem={reasons.find((p) => p.uuid === field.value)}
46
+ itemToString={(item?: Concept) => (item && item?.display ? `${item?.display}` : '')}
47
+ onChange={(data: { selectedItem?: Concept }) => {
48
+ field.onChange(data?.selectedItem?.uuid);
49
+ }}
50
+ ref={field.ref}
51
+ invalid={error?.message}
52
+ invalidText={error?.message}
53
+ />
54
+ )}
55
+ />
56
+ );
57
+ };
58
+
59
+ export default StockOperationReasonSelector;
@@ -0,0 +1,216 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import React from 'react';
4
+
5
+ import { useConfig } from '@openmrs/esm-framework';
6
+ import { useFormContext } from 'react-hook-form';
7
+ import { useConcept } from '../../../stock-lookups/stock-lookups.resource';
8
+ import StockOperationReasonSelector from './stock-operation-reason-selector.component';
9
+
10
+ // Mock the hooks
11
+ jest.mock('@openmrs/esm-framework', () => ({
12
+ useConfig: jest.fn(),
13
+ }));
14
+
15
+ jest.mock('react-hook-form', () => ({
16
+ useFormContext: jest.fn(),
17
+ Controller: ({ render }) => render({ field: {}, fieldState: {} }),
18
+ }));
19
+ jest.mock('../../../stock-lookups/stock-lookups.resource');
20
+ jest.mock('react-i18next', () => ({
21
+ useTranslation: () => ({ t: (key: string) => key }),
22
+ }));
23
+
24
+ const mockUseConcept = useConcept as jest.Mock;
25
+ const mockUseConfig = useConfig as jest.Mock;
26
+ const mockUseFormContext = useFormContext as jest.Mock;
27
+
28
+ describe('StockoperationReasonSelector', () => {
29
+ const mockConcepts = {
30
+ uuid: '3bbfaa44-d5b8-404d-b4c1-2bf49ad8ce25',
31
+ display: 'Stock Adjustment Reason',
32
+ name: {
33
+ display: 'Stock Adjustment Reason',
34
+ uuid: '4eb6556a-a2d4-4e85-9b62-4d076a1063fc',
35
+ name: 'Stock Adjustment Reason',
36
+ locale: 'en',
37
+ localePreferred: true,
38
+ conceptNameType: 'FULLY_SPECIFIED',
39
+ links: [
40
+ {
41
+ rel: 'self',
42
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/3bbfaa44-d5b8-404d-b4c1-2bf49ad8ce25/name/4eb6556a-a2d4-4e85-9b62-4d076a1063fc',
43
+ resourceAlias: 'name',
44
+ },
45
+ {
46
+ rel: 'full',
47
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/3bbfaa44-d5b8-404d-b4c1-2bf49ad8ce25/name/4eb6556a-a2d4-4e85-9b62-4d076a1063fc?v=full',
48
+ resourceAlias: 'name',
49
+ },
50
+ ],
51
+ resourceVersion: '1.9',
52
+ },
53
+ datatype: {
54
+ uuid: '8d4a48b6-c2cc-11de-8d13-0010c6dffd0f',
55
+ display: 'Coded',
56
+ links: [
57
+ {
58
+ rel: 'self',
59
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/conceptdatatype/8d4a48b6-c2cc-11de-8d13-0010c6dffd0f',
60
+ resourceAlias: 'conceptdatatype',
61
+ },
62
+ ],
63
+ },
64
+ conceptClass: {
65
+ uuid: '8d491e50-c2cc-11de-8d13-0010c6dffd0f',
66
+ display: 'Question',
67
+ links: [
68
+ {
69
+ rel: 'self',
70
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/conceptclass/8d491e50-c2cc-11de-8d13-0010c6dffd0f',
71
+ resourceAlias: 'conceptclass',
72
+ },
73
+ ],
74
+ },
75
+ set: false,
76
+ version: null,
77
+ retired: false,
78
+ names: [
79
+ {
80
+ uuid: '4eb6556a-a2d4-4e85-9b62-4d076a1063fc',
81
+ display: 'Stock Adjustment Reason',
82
+ links: [
83
+ {
84
+ rel: 'self',
85
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/3bbfaa44-d5b8-404d-b4c1-2bf49ad8ce25/name/4eb6556a-a2d4-4e85-9b62-4d076a1063fc',
86
+ resourceAlias: 'name',
87
+ },
88
+ ],
89
+ },
90
+ {
91
+ uuid: '36eb0855-c810-4816-9bed-1de5c615e702',
92
+ display: 'Stock Adjustment Reason',
93
+ links: [
94
+ {
95
+ rel: 'self',
96
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/3bbfaa44-d5b8-404d-b4c1-2bf49ad8ce25/name/36eb0855-c810-4816-9bed-1de5c615e702',
97
+ resourceAlias: 'name',
98
+ },
99
+ ],
100
+ },
101
+ ],
102
+ descriptions: [
103
+ {
104
+ uuid: '67311e07-1935-448b-8305-7d11abf0de63',
105
+ display: 'Stock Adjustment Reason',
106
+ links: [
107
+ {
108
+ rel: 'self',
109
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/3bbfaa44-d5b8-404d-b4c1-2bf49ad8ce25/description/67311e07-1935-448b-8305-7d11abf0de63',
110
+ resourceAlias: 'description',
111
+ },
112
+ ],
113
+ },
114
+ ],
115
+ mappings: [],
116
+ answers: [
117
+ {
118
+ uuid: '165420AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
119
+ display: 'Drug not available due to expired medication',
120
+ links: [
121
+ {
122
+ rel: 'self',
123
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/165420AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
124
+ resourceAlias: 'concept',
125
+ },
126
+ ],
127
+ },
128
+ {
129
+ uuid: '160584AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
130
+ display: 'Lost or ran out of medication',
131
+ links: [
132
+ {
133
+ rel: 'self',
134
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/160584AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
135
+ resourceAlias: 'concept',
136
+ },
137
+ ],
138
+ },
139
+ {
140
+ uuid: '122835AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
141
+ display: 'Work Shift Change',
142
+ links: [
143
+ {
144
+ rel: 'self',
145
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/122835AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
146
+ resourceAlias: 'concept',
147
+ },
148
+ ],
149
+ },
150
+ {
151
+ uuid: '160561AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
152
+ display: 'New drug available',
153
+ links: [
154
+ {
155
+ rel: 'self',
156
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/160561AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
157
+ resourceAlias: 'concept',
158
+ },
159
+ ],
160
+ },
161
+ ],
162
+ setMembers: [],
163
+ attributes: [],
164
+ links: [
165
+ {
166
+ rel: 'self',
167
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/3bbfaa44-d5b8-404d-b4c1-2bf49ad8ce25',
168
+ resourceAlias: 'concept',
169
+ },
170
+ {
171
+ rel: 'full',
172
+ uri: 'http://qa.kenyahmis.org/openmrs/ws/rest/v1/concept/3bbfaa44-d5b8-404d-b4c1-2bf49ad8ce25?v=full',
173
+ resourceAlias: 'concept',
174
+ },
175
+ ],
176
+ resourceVersion: '2.0',
177
+ };
178
+ beforeEach(() => {
179
+ jest.clearAllMocks();
180
+ mockUseConfig.mockReturnValue({ stockAdjustmentReasonUUID: 'uuid' });
181
+ mockUseConcept.mockReturnValue({
182
+ isLoading: false,
183
+ items: mockConcepts,
184
+ });
185
+ });
186
+
187
+ it('should display loading state while loading reason concepts', () => {
188
+ mockUseFormContext.mockReturnValue({ control: {} });
189
+ mockUseConcept.mockReturnValue({
190
+ isLoading: true,
191
+ items: mockConcepts,
192
+ });
193
+ render(<StockOperationReasonSelector />);
194
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
195
+ });
196
+ it('should display error notification when error encountered while fetching concepts', () => {
197
+ const errorMessahe = 'Error message';
198
+ mockUseConcept.mockReturnValue({
199
+ isLoading: false,
200
+ items: mockConcepts,
201
+ error: new Error(errorMessahe),
202
+ });
203
+ render(<StockOperationReasonSelector />);
204
+ expect(screen.getByRole('status')).toBeInTheDocument();
205
+ expect(screen.getByText(errorMessahe)).toBeInTheDocument();
206
+ });
207
+
208
+ it('renders ComboBox with reasons', async () => {
209
+ render(<StockOperationReasonSelector />);
210
+ const combobox = screen.getByRole('combobox');
211
+ await userEvent.click(combobox);
212
+ mockConcepts.answers.forEach((ans) => {
213
+ expect(screen.getByText(`${ans?.display}`)).toBeInTheDocument();
214
+ });
215
+ });
216
+ });
@@ -0,0 +1,59 @@
1
+ import { TextInput } from '@carbon/react';
2
+ import React, { ChangeEvent, useEffect, useMemo, useState } from 'react';
3
+ import { TextInputSkeleton } from '@carbon/react';
4
+ import { useStockItemBatchNumbers } from '../hooks/useStockItemBatchNumbers';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ type UniqueBatchNoEntryInputProps = {
8
+ defaultValue?: string;
9
+ onValueChange?: (value: string) => void;
10
+ error?: string;
11
+ stockItemUuid: string;
12
+ stockOperationItemUuid: string;
13
+ };
14
+ const UniqueBatchNoEntryInput: React.FC<UniqueBatchNoEntryInputProps> = ({
15
+ defaultValue,
16
+ onValueChange,
17
+ error,
18
+ stockItemUuid,
19
+ stockOperationItemUuid,
20
+ }) => {
21
+ const { isLoading, stockItemBatchNos } = useStockItemBatchNumbers(stockItemUuid);
22
+ const [value, setValue] = useState(defaultValue);
23
+ const [_error, setError] = useState<string>();
24
+ const { t } = useTranslation();
25
+ const isNewItem = useMemo(() => stockOperationItemUuid.startsWith('new-item'), [stockOperationItemUuid]);
26
+ const batchNoAlreadyUsed = useMemo(
27
+ () => isNewItem && stockItemBatchNos?.findIndex((batchNo) => batchNo.batchNo === value) !== -1,
28
+ [stockItemBatchNos, value, isNewItem],
29
+ );
30
+
31
+ useEffect(() => {
32
+ if (defaultValue) setValue(defaultValue ?? '');
33
+ }, [defaultValue]);
34
+
35
+ useEffect(() => {
36
+ if (batchNoAlreadyUsed) {
37
+ setError('Batch number already used');
38
+ } else {
39
+ setError(undefined);
40
+ onValueChange?.(value);
41
+ }
42
+ }, [value, onValueChange, batchNoAlreadyUsed, setError]);
43
+
44
+ if (isLoading) return <TextInputSkeleton />;
45
+
46
+ return (
47
+ <TextInput
48
+ maxLength={50}
49
+ onChange={(e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}
50
+ value={value}
51
+ invalidText={_error ?? error}
52
+ invalid={_error ?? error}
53
+ placeholder={t('batchNumber', 'Batch Number')}
54
+ labelText={t('batchNumber', 'Batch Number')}
55
+ />
56
+ );
57
+ };
58
+
59
+ export default UniqueBatchNoEntryInput;