@kenyaemr/esm-etims-app 5.4.5-pre.1109
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.
- package/.turbo/turbo-build.log +6 -0
- package/README.md +3 -0
- package/dist/1067.js +1 -0
- package/dist/1067.js.map +1 -0
- package/dist/1072.js +1 -0
- package/dist/1072.js.map +1 -0
- package/dist/1148.js +1 -0
- package/dist/1148.js.map +1 -0
- package/dist/1278.js +1 -0
- package/dist/1278.js.map +1 -0
- package/dist/1442.js +1 -0
- package/dist/1442.js.map +1 -0
- package/dist/15.js +1 -0
- package/dist/15.js.map +1 -0
- package/dist/1620.js +1 -0
- package/dist/1620.js.map +1 -0
- package/dist/1638.js +1 -0
- package/dist/1638.js.map +1 -0
- package/dist/1789.js +1 -0
- package/dist/1789.js.map +1 -0
- package/dist/1831.js +1 -0
- package/dist/1831.js.map +1 -0
- package/dist/1844.js +1 -0
- package/dist/1844.js.map +1 -0
- package/dist/1946.js +1 -0
- package/dist/1946.js.map +1 -0
- package/dist/2007.js +1 -0
- package/dist/2007.js.map +1 -0
- package/dist/2115.js +1 -0
- package/dist/2115.js.map +1 -0
- package/dist/2131.js +1 -0
- package/dist/2131.js.map +1 -0
- package/dist/2152.js +1 -0
- package/dist/2152.js.map +1 -0
- package/dist/2202.js +1 -0
- package/dist/2202.js.map +1 -0
- package/dist/2394.js +1 -0
- package/dist/2394.js.map +1 -0
- package/dist/2534.js +1 -0
- package/dist/2534.js.map +1 -0
- package/dist/2537.js +1 -0
- package/dist/2537.js.map +1 -0
- package/dist/2556.js +1 -0
- package/dist/2556.js.map +1 -0
- package/dist/2621.js +1 -0
- package/dist/2621.js.map +1 -0
- package/dist/2648.js +1 -0
- package/dist/2648.js.map +1 -0
- package/dist/2795.js +1 -0
- package/dist/2795.js.map +1 -0
- package/dist/2842.js +1 -0
- package/dist/2842.js.map +1 -0
- package/dist/2916.js +1 -0
- package/dist/2916.js.map +1 -0
- package/dist/2924.js +1 -0
- package/dist/2924.js.map +1 -0
- package/dist/2929.js +1 -0
- package/dist/2929.js.map +1 -0
- package/dist/2947.js +1 -0
- package/dist/2947.js.map +1 -0
- package/dist/2971.js +1 -0
- package/dist/2971.js.map +1 -0
- package/dist/3284.js +1 -0
- package/dist/3284.js.map +1 -0
- package/dist/3285.js +1 -0
- package/dist/3285.js.map +1 -0
- package/dist/3335.js +1 -0
- package/dist/3335.js.map +1 -0
- package/dist/335.js +1 -0
- package/dist/335.js.map +1 -0
- package/dist/3378.js +1 -0
- package/dist/3378.js.map +1 -0
- package/dist/338.js +1 -0
- package/dist/338.js.map +1 -0
- package/dist/3504.js +1 -0
- package/dist/3504.js.map +1 -0
- package/dist/3544.js +1 -0
- package/dist/3544.js.map +1 -0
- package/dist/356.js +1 -0
- package/dist/356.js.map +1 -0
- package/dist/3569.js +1 -0
- package/dist/3569.js.map +1 -0
- package/dist/3723.js +1 -0
- package/dist/3723.js.map +1 -0
- package/dist/3745.js +1 -0
- package/dist/3745.js.map +1 -0
- package/dist/3813.js +1 -0
- package/dist/3813.js.map +1 -0
- package/dist/393.js +6 -0
- package/dist/393.js.map +1 -0
- package/dist/3949.js +1 -0
- package/dist/3949.js.map +1 -0
- package/dist/3953.js +1 -0
- package/dist/3953.js.map +1 -0
- package/dist/3989.js +1 -0
- package/dist/3989.js.map +1 -0
- package/dist/4178.js +1 -0
- package/dist/4178.js.map +1 -0
- package/dist/4303.js +1 -0
- package/dist/4303.js.map +1 -0
- package/dist/4308.js +1 -0
- package/dist/4308.js.map +1 -0
- package/dist/4487.js +11 -0
- package/dist/4487.js.map +1 -0
- package/dist/4588.js +1 -0
- package/dist/4588.js.map +1 -0
- package/dist/4637.js +1 -0
- package/dist/4637.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/4817.js +1 -0
- package/dist/4817.js.map +1 -0
- package/dist/4818.js +1 -0
- package/dist/4818.js.map +1 -0
- package/dist/500.js +1 -0
- package/dist/500.js.map +1 -0
- package/dist/5086.js +1 -0
- package/dist/5086.js.map +1 -0
- package/dist/5117.js +1 -0
- package/dist/5117.js.map +1 -0
- package/dist/5159.js +1 -0
- package/dist/5159.js.map +1 -0
- package/dist/5285.js +1 -0
- package/dist/5285.js.map +1 -0
- package/dist/5361.js +1 -0
- package/dist/5361.js.map +1 -0
- package/dist/5366.js +1 -0
- package/dist/5366.js.map +1 -0
- package/dist/5484.js +1 -0
- package/dist/5484.js.map +1 -0
- package/dist/5598.js +1 -0
- package/dist/5598.js.map +1 -0
- package/dist/5676.js +1 -0
- package/dist/5676.js.map +1 -0
- package/dist/5694.js +1 -0
- package/dist/5694.js.map +1 -0
- package/dist/5731.js +5 -0
- package/dist/5731.js.map +1 -0
- package/dist/5743.js +1 -0
- package/dist/5743.js.map +1 -0
- package/dist/5825.js +1 -0
- package/dist/5825.js.map +1 -0
- package/dist/5932.js +1 -0
- package/dist/5932.js.map +1 -0
- package/dist/5941.js +1 -0
- package/dist/5941.js.map +1 -0
- package/dist/5967.js +1 -0
- package/dist/5967.js.map +1 -0
- package/dist/6104.js +1 -0
- package/dist/6104.js.map +1 -0
- package/dist/624.js +1 -0
- package/dist/624.js.map +1 -0
- package/dist/6259.js +1 -0
- package/dist/6259.js.map +1 -0
- package/dist/6273.js +1 -0
- package/dist/6273.js.map +1 -0
- package/dist/6337.js +1 -0
- package/dist/6337.js.map +1 -0
- package/dist/6427.js +1 -0
- package/dist/6427.js.map +1 -0
- package/dist/647.js +1 -0
- package/dist/647.js.map +1 -0
- package/dist/6478.js +7 -0
- package/dist/6478.js.map +1 -0
- package/dist/6508.js +1 -0
- package/dist/6508.js.map +1 -0
- package/dist/6567.js +1 -0
- package/dist/6567.js.map +1 -0
- package/dist/6634.js +1 -0
- package/dist/6634.js.map +1 -0
- package/dist/6868.js +1 -0
- package/dist/6868.js.map +1 -0
- package/dist/689.js +1 -0
- package/dist/689.js.map +1 -0
- package/dist/7022.js +1 -0
- package/dist/7022.js.map +1 -0
- package/dist/7101.js +1 -0
- package/dist/7101.js.map +1 -0
- package/dist/7209.js +1 -0
- package/dist/7209.js.map +1 -0
- package/dist/7293.js +1 -0
- package/dist/7293.js.map +1 -0
- package/dist/7360.js +1 -0
- package/dist/7360.js.map +1 -0
- package/dist/7453.js +1 -0
- package/dist/7453.js.map +1 -0
- package/dist/7478.js +1 -0
- package/dist/7478.js.map +1 -0
- package/dist/7782.js +1 -0
- package/dist/7782.js.map +1 -0
- package/dist/7808.js +1 -0
- package/dist/7808.js.map +1 -0
- package/dist/7876.js +1 -0
- package/dist/7876.js.map +1 -0
- package/dist/8028.js +1 -0
- package/dist/8028.js.map +1 -0
- package/dist/8196.js +1 -0
- package/dist/8196.js.map +1 -0
- package/dist/820.js +1 -0
- package/dist/820.js.map +1 -0
- package/dist/8240.js +1 -0
- package/dist/8240.js.map +1 -0
- package/dist/8458.js +1 -0
- package/dist/8458.js.map +1 -0
- package/dist/8472.js +1 -0
- package/dist/8472.js.map +1 -0
- package/dist/8602.js +1 -0
- package/dist/8602.js.map +1 -0
- package/dist/8607.js +1 -0
- package/dist/8607.js.map +1 -0
- package/dist/8640.js +1 -0
- package/dist/8640.js.map +1 -0
- package/dist/8762.js +1 -0
- package/dist/8762.js.map +1 -0
- package/dist/9060.js +1 -0
- package/dist/9060.js.map +1 -0
- package/dist/9061.js +1 -0
- package/dist/9061.js.map +1 -0
- package/dist/9087.js +1 -0
- package/dist/9087.js.map +1 -0
- package/dist/9168.js +1 -0
- package/dist/9168.js.map +1 -0
- package/dist/9213.js +1 -0
- package/dist/9213.js.map +1 -0
- package/dist/9286.js +1 -0
- package/dist/9286.js.map +1 -0
- package/dist/9425.js +1 -0
- package/dist/9520.js +1 -0
- package/dist/9520.js.map +1 -0
- package/dist/9547.js +1 -0
- package/dist/9547.js.map +1 -0
- package/dist/9612.js +1 -0
- package/dist/9612.js.map +1 -0
- package/dist/9632.js +1 -0
- package/dist/9632.js.map +1 -0
- package/dist/9771.js +1 -0
- package/dist/9771.js.map +1 -0
- package/dist/9774.js +1 -0
- package/dist/9774.js.map +1 -0
- package/dist/9806.js +1 -0
- package/dist/9806.js.map +1 -0
- package/dist/9885.js +1 -0
- package/dist/9885.js.map +1 -0
- package/dist/992.js +1 -0
- package/dist/992.js.map +1 -0
- package/dist/kenyaemr-esm-etims-app.js +3 -0
- package/dist/kenyaemr-esm-etims-app.js.buildmanifest.json +3354 -0
- package/dist/kenyaemr-esm-etims-app.js.map +1 -0
- package/dist/main.js +3 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/package.json +48 -0
- package/rspack.config.js +1 -0
- package/src/components/header/header.component.tsx +22 -0
- package/src/components/header/header.scss +6 -0
- package/src/components/side-menu/left-panel.component.tsx +22 -0
- package/src/components/side-menu/left-panel.scss +34 -0
- package/src/components/user-menu-link/etims-nav-link.scss +29 -0
- package/src/components/user-menu-link/user-menu-link.component.tsx +25 -0
- package/src/config-schema.ts +1 -0
- package/src/credit-note/etims-credit-note-dashboard.component.tsx +23 -0
- package/src/credit-note/etims-credit-note-dashboard.scss +8 -0
- package/src/credit-note/etims-credit-note-table.component.tsx +284 -0
- package/src/credit-note/etims-credit-note-table.scss +16 -0
- package/src/credit-note/etims-credit-note.resource.ts +96 -0
- package/src/credit-note/etims-credit-note.types.ts +56 -0
- package/src/credit-note/etims-credit-note.workspace.scss +76 -0
- package/src/credit-note/etims-credit-note.workspace.tsx +267 -0
- package/src/declarations.d.ts +2 -0
- package/src/global-properties/etims-global-properties-dashboard.component.tsx +141 -0
- package/src/global-properties/etims-global-properties-dashboard.scss +62 -0
- package/src/global-properties/etims-global-properties-form.workspace.scss +74 -0
- package/src/global-properties/etims-global-properties-form.workspace.tsx +244 -0
- package/src/global-properties/etims-global-properties.resource.ts +60 -0
- package/src/index.ts +100 -0
- package/src/item-mapping/etims-item-mapping-dashboard.component.tsx +29 -0
- package/src/item-mapping/etims-item-mapping-dashboard.scss +42 -0
- package/src/item-mapping/etims-item-mapping-form.fields.tsx +136 -0
- package/src/item-mapping/etims-item-mapping-form.schema.ts +35 -0
- package/src/item-mapping/etims-item-mapping-form.workspace.scss +54 -0
- package/src/item-mapping/etims-item-mapping-form.workspace.tsx +555 -0
- package/src/item-mapping/etims-item-mapping-table.component.tsx +398 -0
- package/src/item-mapping/etims-item-mapping-table.scss +20 -0
- package/src/item-mapping/etims-item-mapping.resource.ts +318 -0
- package/src/item-mapping/etims-item-mapping.types.ts +92 -0
- package/src/item-mapping/register-item-mapping.modal.tsx +113 -0
- package/src/item-mapping/void-item-mapping.modal.tsx +80 -0
- package/src/left-panel-link.component.tsx +39 -0
- package/src/left-panel-link.scss +12 -0
- package/src/reference-data/etims-catalog-table.component.tsx +178 -0
- package/src/reference-data/etims-catalog-table.scss +16 -0
- package/src/reference-data/etims-code-table.component.tsx +87 -0
- package/src/reference-data/etims-item-class-table.component.tsx +86 -0
- package/src/reference-data/etims-reference-data-dashboard.component.tsx +38 -0
- package/src/reference-data/etims-reference-data-dashboard.scss +11 -0
- package/src/reference-data/etims-reference-data.resource.ts +40 -0
- package/src/reference-data/etims-reference-data.types.ts +38 -0
- package/src/reference-data/etims-sync-meta.modal.tsx +62 -0
- package/src/root.component.tsx +38 -0
- package/src/root.scss +11 -0
- package/src/routes.json +117 -0
- package/src/status/etims-status-dashboard.component.tsx +212 -0
- package/src/status/etims-status-dashboard.scss +116 -0
- package/src/status/etims-status.resource.ts +20 -0
- package/src/status/etims-status.types.ts +30 -0
- package/src/transaction/confirm-transaction-action.modal.tsx +113 -0
- package/src/transaction/etims-transaction-dashboard.component.tsx +23 -0
- package/src/transaction/etims-transaction-dashboard.scss +8 -0
- package/src/transaction/etims-transaction-details.workspace.scss +76 -0
- package/src/transaction/etims-transaction-details.workspace.tsx +201 -0
- package/src/transaction/etims-transaction-table.component.tsx +450 -0
- package/src/transaction/etims-transaction-table.scss +20 -0
- package/src/transaction/etims-transaction.resource.ts +180 -0
- package/src/transaction/etims-transaction.types.ts +78 -0
- package/src/transaction/override-reconciliation.modal.scss +7 -0
- package/src/transaction/override-reconciliation.modal.tsx +203 -0
- package/translations/am.json +129 -0
- package/translations/en.json +129 -0
- package/translations/fr.json +130 -0
- package/translations/sw.json +129 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
ButtonSet,
|
|
5
|
+
Form,
|
|
6
|
+
InlineLoading,
|
|
7
|
+
PasswordInput,
|
|
8
|
+
Select,
|
|
9
|
+
SelectItem,
|
|
10
|
+
Stack,
|
|
11
|
+
TextInput,
|
|
12
|
+
Toggle,
|
|
13
|
+
} from '@carbon/react';
|
|
14
|
+
import {
|
|
15
|
+
ErrorState,
|
|
16
|
+
showSnackbar,
|
|
17
|
+
useLayoutType,
|
|
18
|
+
Workspace2,
|
|
19
|
+
type Workspace2DefinitionProps,
|
|
20
|
+
} from '@openmrs/esm-framework';
|
|
21
|
+
import classNames from 'classnames';
|
|
22
|
+
import { useTranslation } from 'react-i18next';
|
|
23
|
+
import {
|
|
24
|
+
saveEtimsGlobalProperty,
|
|
25
|
+
etimsPropertyKeys,
|
|
26
|
+
useEtimsGlobalProperties,
|
|
27
|
+
type EtimsPropertyName,
|
|
28
|
+
type EtimsGlobalProperty,
|
|
29
|
+
} from './etims-global-properties.resource';
|
|
30
|
+
import styles from './etims-global-properties-form.workspace.scss';
|
|
31
|
+
|
|
32
|
+
type FormValues = Record<EtimsPropertyName, string>;
|
|
33
|
+
|
|
34
|
+
const emptyValues = Object.fromEntries(Object.keys(etimsPropertyKeys).map((key) => [key, ''])) as FormValues;
|
|
35
|
+
|
|
36
|
+
const propertiesToValues = (properties: EtimsGlobalProperty[]): FormValues => {
|
|
37
|
+
const values = { ...emptyValues };
|
|
38
|
+
Object.entries(etimsPropertyKeys).forEach(([name, property]) => {
|
|
39
|
+
values[name as EtimsPropertyName] = properties.find((candidate) => candidate.property === property)?.value ?? '';
|
|
40
|
+
});
|
|
41
|
+
return values;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type EtimsGlobalPropertiesWorkspaceProps = {
|
|
45
|
+
mutateProperties?: () => void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const EtimsGlobalPropertiesFormWorkspace: React.FC<
|
|
49
|
+
Workspace2DefinitionProps<EtimsGlobalPropertiesWorkspaceProps, {}, {}>
|
|
50
|
+
> = ({ closeWorkspace, workspaceProps }) => {
|
|
51
|
+
const { t } = useTranslation();
|
|
52
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
53
|
+
const { properties, error, isLoading, mutate } = useEtimsGlobalProperties();
|
|
54
|
+
const [values, setValues] = useState<FormValues>(emptyValues);
|
|
55
|
+
const [savedValues, setSavedValues] = useState<FormValues>(emptyValues);
|
|
56
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!isLoading) {
|
|
60
|
+
const nextValues = propertiesToValues(properties);
|
|
61
|
+
setValues(nextValues);
|
|
62
|
+
setSavedValues(nextValues);
|
|
63
|
+
}
|
|
64
|
+
}, [isLoading, properties]);
|
|
65
|
+
|
|
66
|
+
const isDirty = useMemo(() => JSON.stringify(values) !== JSON.stringify(savedValues), [savedValues, values]);
|
|
67
|
+
const updateValue = (name: EtimsPropertyName, value: string) =>
|
|
68
|
+
setValues((current) => ({ ...current, [name]: value }));
|
|
69
|
+
|
|
70
|
+
const handleSubmit = async (event: React.FormEvent) => {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
setIsSaving(true);
|
|
73
|
+
try {
|
|
74
|
+
const changedProperties = Object.entries(etimsPropertyKeys).filter(([name, property]) => {
|
|
75
|
+
const exists = properties.some((candidate) => candidate.property === property);
|
|
76
|
+
return !exists || values[name as EtimsPropertyName] !== savedValues[name as EtimsPropertyName];
|
|
77
|
+
});
|
|
78
|
+
await Promise.all(
|
|
79
|
+
changedProperties.map(([name, property]) =>
|
|
80
|
+
saveEtimsGlobalProperty(
|
|
81
|
+
property,
|
|
82
|
+
values[name as EtimsPropertyName],
|
|
83
|
+
properties.find((candidate) => candidate.property === property)?.uuid,
|
|
84
|
+
),
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
await mutate();
|
|
88
|
+
workspaceProps?.mutateProperties?.();
|
|
89
|
+
setSavedValues(values);
|
|
90
|
+
showSnackbar({
|
|
91
|
+
kind: 'success',
|
|
92
|
+
title: t('etimsConfigurationSaved', 'eTIMS configuration saved'),
|
|
93
|
+
subtitle: t('etimsConfigurationSavedMessage', 'The eTIMS global properties were updated successfully.'),
|
|
94
|
+
});
|
|
95
|
+
closeWorkspace({ discardUnsavedChanges: true });
|
|
96
|
+
} catch (saveError: any) {
|
|
97
|
+
showSnackbar({
|
|
98
|
+
kind: 'error',
|
|
99
|
+
title: t('errorSavingEtimsConfiguration', 'Error saving eTIMS configuration'),
|
|
100
|
+
subtitle: saveError?.message,
|
|
101
|
+
});
|
|
102
|
+
} finally {
|
|
103
|
+
setIsSaving(false);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const textField = (name: EtimsPropertyName, label: string, helperText?: string, type?: 'number') => (
|
|
108
|
+
<TextInput
|
|
109
|
+
id={`etims-${name}`}
|
|
110
|
+
labelText={label}
|
|
111
|
+
helperText={helperText}
|
|
112
|
+
type={type ?? 'text'}
|
|
113
|
+
min={type === 'number' ? 0 : undefined}
|
|
114
|
+
value={values[name]}
|
|
115
|
+
onChange={(event) => updateValue(name, event.target.value)}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Workspace2
|
|
121
|
+
title={t('configureEtimsGlobalProperties', 'Configure eTIMS global properties')}
|
|
122
|
+
hasUnsavedChanges={isDirty}>
|
|
123
|
+
{isLoading ? (
|
|
124
|
+
<InlineLoading description={t('loadingEtimsConfiguration', 'Loading eTIMS configuration...')} />
|
|
125
|
+
) : error ? (
|
|
126
|
+
<ErrorState error={error} headerTitle={t('etimsGlobalProperties', 'eTIMS global properties')} />
|
|
127
|
+
) : (
|
|
128
|
+
<Form className={styles.form} onSubmit={handleSubmit}>
|
|
129
|
+
<div className={styles.formContainer}>
|
|
130
|
+
<Stack gap={5}>
|
|
131
|
+
<p className={styles.intro}>
|
|
132
|
+
{t(
|
|
133
|
+
'etimsGlobalPropertiesDescription',
|
|
134
|
+
'Configure the connection, resilience, invoicing, and integration settings used by eTIMS.',
|
|
135
|
+
)}
|
|
136
|
+
</p>
|
|
137
|
+
<section className={styles.section}>
|
|
138
|
+
<h2 className={styles.sectionTitle}>{t('connectionAndCredentials', 'Connection and credentials')}</h2>
|
|
139
|
+
<div className={styles.fields}>
|
|
140
|
+
<div className={styles.fullWidth}>
|
|
141
|
+
<Toggle
|
|
142
|
+
id="etims-enabled"
|
|
143
|
+
labelText={t('etimsEnabled', 'eTIMS enabled')}
|
|
144
|
+
labelA={t('no', 'No')}
|
|
145
|
+
labelB={t('yes', 'Yes')}
|
|
146
|
+
toggled={values.enabled === 'true'}
|
|
147
|
+
onToggle={(enabled) => updateValue('enabled', String(enabled))}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
<Select
|
|
151
|
+
id="etims-environment"
|
|
152
|
+
labelText={t('environment', 'Environment')}
|
|
153
|
+
value={values.environment}
|
|
154
|
+
onChange={(event) => updateValue('environment', event.target.value)}>
|
|
155
|
+
<SelectItem value="" text={t('selectEnvironment', 'Select an environment')} />
|
|
156
|
+
<SelectItem value="sandbox" text={t('sandbox', 'Sandbox')} />
|
|
157
|
+
<SelectItem value="production" text={t('production', 'Production')} />
|
|
158
|
+
</Select>
|
|
159
|
+
{textField('baseUrl', t('baseUrl', 'Base URL'))}
|
|
160
|
+
{textField('businessPin', t('businessPin', 'Business PIN'))}
|
|
161
|
+
{textField('branchId', t('branchId', 'Branch ID'))}
|
|
162
|
+
<PasswordInput
|
|
163
|
+
id="etims-apiKey"
|
|
164
|
+
labelText={t('apiKey', 'API key')}
|
|
165
|
+
value={values.apiKey}
|
|
166
|
+
onChange={(event) => updateValue('apiKey', event.target.value)}
|
|
167
|
+
/>
|
|
168
|
+
{textField(
|
|
169
|
+
'sandboxValidatedContractVersion',
|
|
170
|
+
t('sandboxValidatedContractVersion', 'Sandbox validated contract version'),
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
</section>
|
|
174
|
+
<section className={styles.section}>
|
|
175
|
+
<h2 className={styles.sectionTitle}>{t('timeoutsAndRetries', 'Timeouts and retries')}</h2>
|
|
176
|
+
<div className={styles.fields}>
|
|
177
|
+
{textField(
|
|
178
|
+
'connectTimeout',
|
|
179
|
+
t('connectTimeout', 'Connect timeout'),
|
|
180
|
+
t('milliseconds', 'Milliseconds'),
|
|
181
|
+
'number',
|
|
182
|
+
)}
|
|
183
|
+
{textField(
|
|
184
|
+
'readTimeout',
|
|
185
|
+
t('readTimeout', 'Read timeout'),
|
|
186
|
+
t('milliseconds', 'Milliseconds'),
|
|
187
|
+
'number',
|
|
188
|
+
)}
|
|
189
|
+
{textField('maxAttempts', t('maxAttempts', 'Maximum attempts'), undefined, 'number')}
|
|
190
|
+
{textField(
|
|
191
|
+
'retryInterval',
|
|
192
|
+
t('retryInterval', 'Retry interval'),
|
|
193
|
+
t('milliseconds', 'Milliseconds'),
|
|
194
|
+
'number',
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
</section>
|
|
198
|
+
<section className={styles.section}>
|
|
199
|
+
<h2 className={styles.sectionTitle}>{t('invoiceAndPayment', 'Invoice and payment')}</h2>
|
|
200
|
+
<div className={styles.fields}>
|
|
201
|
+
{textField('invoiceNumberSource', t('invoiceNumberSource', 'Invoice number source'))}
|
|
202
|
+
{textField(
|
|
203
|
+
'localInvoiceNumberStart',
|
|
204
|
+
t('localInvoiceNumberStart', 'Local invoice number start'),
|
|
205
|
+
undefined,
|
|
206
|
+
'number',
|
|
207
|
+
)}
|
|
208
|
+
<div className={styles.fullWidth}>
|
|
209
|
+
{textField(
|
|
210
|
+
'paymentModeMappingFile',
|
|
211
|
+
t('paymentModeMappingFile', 'Payment mode mapping file'),
|
|
212
|
+
t('paymentModeMappingFileHelp', 'Path to the payment mode mapping configuration file.'),
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</section>
|
|
217
|
+
</Stack>
|
|
218
|
+
</div>
|
|
219
|
+
<ButtonSet
|
|
220
|
+
className={classNames({
|
|
221
|
+
[styles.tablet]: isTablet,
|
|
222
|
+
[styles.desktop]: !isTablet,
|
|
223
|
+
})}>
|
|
224
|
+
<Button className={styles.buttonContainer} kind="secondary" type="button" onClick={() => closeWorkspace()}>
|
|
225
|
+
{t('cancel', 'Cancel')}
|
|
226
|
+
</Button>
|
|
227
|
+
<Button className={styles.buttonContainer} type="submit" disabled={!isDirty || isSaving}>
|
|
228
|
+
{isSaving ? (
|
|
229
|
+
<span className={styles.inlineLoading}>
|
|
230
|
+
{t('saving', 'Saving...')}
|
|
231
|
+
<InlineLoading status="active" iconDescription="Loading" />
|
|
232
|
+
</span>
|
|
233
|
+
) : (
|
|
234
|
+
t('save', 'Save')
|
|
235
|
+
)}
|
|
236
|
+
</Button>
|
|
237
|
+
</ButtonSet>
|
|
238
|
+
</Form>
|
|
239
|
+
)}
|
|
240
|
+
</Workspace2>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export default EtimsGlobalPropertiesFormWorkspace;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
import useSWR from 'swr';
|
|
3
|
+
|
|
4
|
+
export interface EtimsGlobalProperty {
|
|
5
|
+
uuid: string;
|
|
6
|
+
property: string;
|
|
7
|
+
value: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface SystemSettingResponse {
|
|
11
|
+
results: EtimsGlobalProperty[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ETIMS_PROPERTY_PREFIX = 'kenyaemr.cashier.etims.';
|
|
15
|
+
export const etimsPropertyKeys = {
|
|
16
|
+
enabled: `${ETIMS_PROPERTY_PREFIX}enabled`,
|
|
17
|
+
environment: `${ETIMS_PROPERTY_PREFIX}environment`,
|
|
18
|
+
baseUrl: `${ETIMS_PROPERTY_PREFIX}baseUrl`,
|
|
19
|
+
businessPin: `${ETIMS_PROPERTY_PREFIX}businessPin`,
|
|
20
|
+
branchId: `${ETIMS_PROPERTY_PREFIX}branchId`,
|
|
21
|
+
connectTimeout: `${ETIMS_PROPERTY_PREFIX}connectTimeout`,
|
|
22
|
+
readTimeout: `${ETIMS_PROPERTY_PREFIX}readTimeout`,
|
|
23
|
+
maxAttempts: `${ETIMS_PROPERTY_PREFIX}maxAttempts`,
|
|
24
|
+
retryInterval: `${ETIMS_PROPERTY_PREFIX}retryInterval`,
|
|
25
|
+
invoiceNumberSource: `${ETIMS_PROPERTY_PREFIX}invoiceNumberSource`,
|
|
26
|
+
localInvoiceNumberStart: `${ETIMS_PROPERTY_PREFIX}localInvoiceNumberStart`,
|
|
27
|
+
paymentModeMappingFile: `${ETIMS_PROPERTY_PREFIX}paymentModeMappingFile`,
|
|
28
|
+
apiKey: `${ETIMS_PROPERTY_PREFIX}apiKey`,
|
|
29
|
+
sandboxValidatedContractVersion: `${ETIMS_PROPERTY_PREFIX}sandboxValidatedContractVersion`,
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export type EtimsPropertyName = keyof typeof etimsPropertyKeys;
|
|
33
|
+
const emptyProperties: EtimsGlobalProperty[] = [];
|
|
34
|
+
|
|
35
|
+
export const useEtimsGlobalProperties = () => {
|
|
36
|
+
const url = `${restBaseUrl}/systemsetting?q=${encodeURIComponent(
|
|
37
|
+
ETIMS_PROPERTY_PREFIX,
|
|
38
|
+
)}&v=custom:(uuid,property,value)`;
|
|
39
|
+
const { data, error, isLoading, mutate } = useSWR<FetchResponse<SystemSettingResponse>>(url, openmrsFetch);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
properties: data?.data?.results ?? emptyProperties,
|
|
43
|
+
error,
|
|
44
|
+
isLoading,
|
|
45
|
+
mutate,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export async function saveEtimsGlobalProperty(property: string, value: string, uuid?: string): Promise<void> {
|
|
50
|
+
const response = await openmrsFetch(uuid ? `${restBaseUrl}/systemsetting/${uuid}` : `${restBaseUrl}/systemsetting`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({ property, value }),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
const responseBody = await response.json().catch(() => ({}));
|
|
58
|
+
throw new Error(responseBody?.error?.message ?? `Failed to save ${property}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type ComponentType } from 'react';
|
|
2
|
+
import { defineConfigSchema, getAsyncLifecycle, type IconId } from '@openmrs/esm-framework';
|
|
3
|
+
import { configSchema } from './config-schema';
|
|
4
|
+
|
|
5
|
+
const moduleName = '@kenyaemr/esm-etims-app';
|
|
6
|
+
|
|
7
|
+
const options = {
|
|
8
|
+
featureName: 'esm-etims-app',
|
|
9
|
+
moduleName,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
|
|
13
|
+
|
|
14
|
+
export const root = getAsyncLifecycle(() => import('./root.component'), options);
|
|
15
|
+
|
|
16
|
+
const loadEtimsLeftPanelLink =
|
|
17
|
+
(title: string, name: string, icon: IconId) => (): Promise<{ default: ComponentType<unknown> }> =>
|
|
18
|
+
import('./left-panel-link.component').then((module) => ({
|
|
19
|
+
default: module.createLeftPanelLink({
|
|
20
|
+
title,
|
|
21
|
+
name,
|
|
22
|
+
icon,
|
|
23
|
+
}),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
// t('status', 'Status')
|
|
27
|
+
export const etimsLeftPanelLink = getAsyncLifecycle(
|
|
28
|
+
loadEtimsLeftPanelLink('status', '', 'omrs-icon-activity'),
|
|
29
|
+
options,
|
|
30
|
+
);
|
|
31
|
+
// t('referenceData', 'Reference data')
|
|
32
|
+
export const etimsReferenceDataLeftPanelLink = getAsyncLifecycle(
|
|
33
|
+
loadEtimsLeftPanelLink('referenceData', 'reference-data', 'omrs-icon-report'),
|
|
34
|
+
options,
|
|
35
|
+
);
|
|
36
|
+
// t('itemMappings', 'Item mappings')
|
|
37
|
+
export const etimsItemMappingLeftPanelLink = getAsyncLifecycle(
|
|
38
|
+
loadEtimsLeftPanelLink('itemMappings', 'item-mappings', 'omrs-icon-inventory-management'),
|
|
39
|
+
options,
|
|
40
|
+
);
|
|
41
|
+
// t('transactions', 'Transactions')
|
|
42
|
+
export const etimsTransactionLeftPanelLink = getAsyncLifecycle(
|
|
43
|
+
loadEtimsLeftPanelLink('transactions', 'transactions', 'omrs-icon-document'),
|
|
44
|
+
options,
|
|
45
|
+
);
|
|
46
|
+
// t('creditNotes', 'Credit notes')
|
|
47
|
+
export const etimsCreditNoteLeftPanelLink = getAsyncLifecycle(
|
|
48
|
+
loadEtimsLeftPanelLink('creditNotes', 'credit-notes', 'omrs-icon-money'),
|
|
49
|
+
options,
|
|
50
|
+
);
|
|
51
|
+
// t('eTimsConfigs', 'Etims Configs')
|
|
52
|
+
export const etimsGlobalPropertiesLeftPanelLink = getAsyncLifecycle(
|
|
53
|
+
loadEtimsLeftPanelLink('eTimsConfigs', 'global-properties', 'omrs-icon-settings'),
|
|
54
|
+
options,
|
|
55
|
+
);
|
|
56
|
+
export const etimsItemMappingForm = getAsyncLifecycle(
|
|
57
|
+
() => import('./item-mapping/etims-item-mapping-form.workspace'),
|
|
58
|
+
options,
|
|
59
|
+
);
|
|
60
|
+
export const etimsGlobalPropertiesForm = getAsyncLifecycle(
|
|
61
|
+
() => import('./global-properties/etims-global-properties-form.workspace'),
|
|
62
|
+
options,
|
|
63
|
+
);
|
|
64
|
+
export const etimsVoidItemMappingModal = getAsyncLifecycle(
|
|
65
|
+
() => import('./item-mapping/void-item-mapping.modal'),
|
|
66
|
+
options,
|
|
67
|
+
);
|
|
68
|
+
export const etimsRegisterItemMappingModal = getAsyncLifecycle(
|
|
69
|
+
() => import('./item-mapping/register-item-mapping.modal'),
|
|
70
|
+
options,
|
|
71
|
+
);
|
|
72
|
+
export const etimsTransactionDetails = getAsyncLifecycle(
|
|
73
|
+
() => import('./transaction/etims-transaction-details.workspace'),
|
|
74
|
+
options,
|
|
75
|
+
);
|
|
76
|
+
export const etimsCreditNoteForm = getAsyncLifecycle(
|
|
77
|
+
() => import('./credit-note/etims-credit-note.workspace'),
|
|
78
|
+
options,
|
|
79
|
+
);
|
|
80
|
+
export const etimsConfirmTransactionActionModal = getAsyncLifecycle(
|
|
81
|
+
() => import('./transaction/confirm-transaction-action.modal'),
|
|
82
|
+
options,
|
|
83
|
+
);
|
|
84
|
+
export const etimsOverrideReconciliationModal = getAsyncLifecycle(
|
|
85
|
+
() => import('./transaction/override-reconciliation.modal'),
|
|
86
|
+
options,
|
|
87
|
+
);
|
|
88
|
+
export const etimsNavLink = getAsyncLifecycle(
|
|
89
|
+
() => import('./components/user-menu-link/user-menu-link.component'),
|
|
90
|
+
options,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
export const etimsSyncMetaDataModal = getAsyncLifecycle(
|
|
94
|
+
() => import('./reference-data/etims-sync-meta.modal'),
|
|
95
|
+
options,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
export function startupApp() {
|
|
99
|
+
defineConfigSchema(moduleName, configSchema);
|
|
100
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { UserHasAccess } from '@openmrs/esm-framework';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import Header from '../components/header/header.component';
|
|
5
|
+
import EtimsItemMappingTable from './etims-item-mapping-table.component';
|
|
6
|
+
import styles from './etims-item-mapping-dashboard.scss';
|
|
7
|
+
|
|
8
|
+
const EtimsItemMappingContent: React.FC = () => {
|
|
9
|
+
return (
|
|
10
|
+
<div className={styles.dashboard}>
|
|
11
|
+
<EtimsItemMappingTable />
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const EtimsItemMappingDashboard: React.FC = () => {
|
|
17
|
+
const { t } = useTranslation();
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className={styles.dashboardContainer}>
|
|
21
|
+
<Header title={t('itemMappings', 'Item mappings')} />
|
|
22
|
+
<UserHasAccess privilege="Manage eTIMS Configuration">
|
|
23
|
+
<EtimsItemMappingContent />
|
|
24
|
+
</UserHasAccess>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default EtimsItemMappingDashboard;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.dashboard {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: layout.$spacing-05;
|
|
9
|
+
padding: layout.$spacing-05;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.cardsRow {
|
|
13
|
+
display: grid;
|
|
14
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
15
|
+
gap: layout.$spacing-05;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.tile {
|
|
19
|
+
border: 1px solid colors.$gray-20;
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: space-between;
|
|
23
|
+
gap: layout.$spacing-03;
|
|
24
|
+
min-height: layout.$spacing-10;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.tileLabel {
|
|
28
|
+
@include type.type-style('heading-compact-01');
|
|
29
|
+
color: colors.$gray-70;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
:global(.omrs-breakpoint-lt-desktop) {
|
|
33
|
+
.cardsRow {
|
|
34
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
:global(.omrs-breakpoint-lt-tablet) {
|
|
39
|
+
.cardsRow {
|
|
40
|
+
grid-template-columns: 1fr;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { ComboBox, InlineLoading } from '@carbon/react';
|
|
3
|
+
import { ResponsiveWrapper } from '@openmrs/esm-framework';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { formatCodeOption, resolveCodeOption } from './etims-item-mapping.resource';
|
|
6
|
+
import type { CodeOption } from './etims-item-mapping.types';
|
|
7
|
+
import styles from './etims-item-mapping-form.workspace.scss';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Keeps the currently selected item visible in the list even when it is not part of the latest
|
|
11
|
+
* async search results (e.g. after a debounced query returns a narrower set).
|
|
12
|
+
*/
|
|
13
|
+
const includeSelectedItem = <T,>(items: T[], selected: T | null | undefined, getKey: (item: T) => string) => {
|
|
14
|
+
if (!selected) {
|
|
15
|
+
return items;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const selectedKey = getKey(selected);
|
|
19
|
+
if (!selectedKey || items.some((item) => getKey(item) === selectedKey)) {
|
|
20
|
+
return items;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return [selected, ...items];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Carbon's ComboBox echoes the selected item's label back through `onInputChange`. This detects that
|
|
28
|
+
* echo so it isn't mistaken for a new search term.
|
|
29
|
+
*/
|
|
30
|
+
const isSelectionEcho = <T,>(value: string, items: T[], itemToString: (item?: T | null) => string) =>
|
|
31
|
+
items.some((item) => itemToString(item) === value);
|
|
32
|
+
|
|
33
|
+
interface AsyncSearchComboBoxProps<T> {
|
|
34
|
+
id: string;
|
|
35
|
+
titleText: string;
|
|
36
|
+
placeholder: string;
|
|
37
|
+
items: T[];
|
|
38
|
+
selectedItem: T | null;
|
|
39
|
+
itemToString: (item?: T | null) => string;
|
|
40
|
+
getKey: (item: T) => string;
|
|
41
|
+
isLoading?: boolean;
|
|
42
|
+
onSearch: (value: string) => void;
|
|
43
|
+
onSelect: (item: T | null) => void;
|
|
44
|
+
invalid?: boolean;
|
|
45
|
+
invalidText?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A server-side searchable ComboBox that surfaces a loading indicator and preserves the current
|
|
50
|
+
* selection across searches. Used for billable services, stock items and item classes.
|
|
51
|
+
*/
|
|
52
|
+
export function AsyncSearchComboBox<T>({
|
|
53
|
+
id,
|
|
54
|
+
titleText,
|
|
55
|
+
placeholder,
|
|
56
|
+
items,
|
|
57
|
+
selectedItem,
|
|
58
|
+
itemToString,
|
|
59
|
+
getKey,
|
|
60
|
+
isLoading,
|
|
61
|
+
onSearch,
|
|
62
|
+
onSelect,
|
|
63
|
+
invalid,
|
|
64
|
+
invalidText,
|
|
65
|
+
}: AsyncSearchComboBoxProps<T>) {
|
|
66
|
+
const { t } = useTranslation();
|
|
67
|
+
const mergedItems = useMemo(() => includeSelectedItem(items, selectedItem, getKey), [items, selectedItem, getKey]);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<ResponsiveWrapper>
|
|
71
|
+
<div className={styles.searchField}>
|
|
72
|
+
<ComboBox<T>
|
|
73
|
+
id={id}
|
|
74
|
+
titleText={titleText}
|
|
75
|
+
placeholder={placeholder}
|
|
76
|
+
items={mergedItems}
|
|
77
|
+
itemToString={itemToString}
|
|
78
|
+
selectedItem={selectedItem}
|
|
79
|
+
onInputChange={(value) => {
|
|
80
|
+
if (isSelectionEcho(value, mergedItems, itemToString)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
onSearch(value);
|
|
84
|
+
}}
|
|
85
|
+
shouldFilterItem={() => true}
|
|
86
|
+
onChange={({ selectedItem: selection }) => onSelect(selection ?? null)}
|
|
87
|
+
invalid={invalid}
|
|
88
|
+
invalidText={invalidText}
|
|
89
|
+
/>
|
|
90
|
+
<div className={styles.searchStatus}>
|
|
91
|
+
{isLoading ? <InlineLoading description={t('searching', 'Searching...')} /> : null}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</ResponsiveWrapper>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface CodeComboBoxProps {
|
|
99
|
+
id: string;
|
|
100
|
+
titleText: string;
|
|
101
|
+
placeholder: string;
|
|
102
|
+
items: CodeOption[];
|
|
103
|
+
value: string;
|
|
104
|
+
onChange: (code: string) => void;
|
|
105
|
+
invalid?: boolean;
|
|
106
|
+
invalidText?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A ComboBox backed by eTIMS code-list options (tax type, item type, quantity/packaging unit,
|
|
111
|
+
* country). The form stores the plain code string while the control renders the resolved option.
|
|
112
|
+
*/
|
|
113
|
+
export function CodeComboBox({
|
|
114
|
+
id,
|
|
115
|
+
titleText,
|
|
116
|
+
placeholder,
|
|
117
|
+
items,
|
|
118
|
+
value,
|
|
119
|
+
onChange,
|
|
120
|
+
invalid,
|
|
121
|
+
invalidText,
|
|
122
|
+
}: CodeComboBoxProps) {
|
|
123
|
+
return (
|
|
124
|
+
<ComboBox<CodeOption>
|
|
125
|
+
id={id}
|
|
126
|
+
titleText={titleText}
|
|
127
|
+
placeholder={placeholder}
|
|
128
|
+
items={items}
|
|
129
|
+
itemToString={formatCodeOption}
|
|
130
|
+
selectedItem={resolveCodeOption(items, value)}
|
|
131
|
+
onChange={({ selectedItem }) => onChange(selectedItem?.code ?? '')}
|
|
132
|
+
invalid={invalid}
|
|
133
|
+
invalidText={invalidText}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const etimsItemMappingFormSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
sourceType: z.enum(['billableService', 'stockItem']),
|
|
6
|
+
billableServiceId: z.number().int().positive().nullable().optional(),
|
|
7
|
+
stockItemId: z.number().int().positive().nullable().optional(),
|
|
8
|
+
itemName: z.string().trim().min(1, 'Item name is required'),
|
|
9
|
+
itemClassCode: z.string().trim().min(1, 'Item class is required'),
|
|
10
|
+
taxType: z.string().trim().min(1, 'Tax type is required'),
|
|
11
|
+
itemType: z.string().trim().min(1, 'Item type is required'),
|
|
12
|
+
countryCode: z.string().trim().min(1, 'Country is required'),
|
|
13
|
+
quantityUnit: z.string().trim().min(1, 'Quantity unit is required'),
|
|
14
|
+
packagingUnit: z.string().trim().min(1, 'Packaging unit is required'),
|
|
15
|
+
defaultSellingPrice: z.number().nonnegative().nullable().optional(),
|
|
16
|
+
})
|
|
17
|
+
.superRefine((value, context) => {
|
|
18
|
+
if (value.sourceType === 'billableService' && !value.billableServiceId) {
|
|
19
|
+
context.addIssue({
|
|
20
|
+
code: z.ZodIssueCode.custom,
|
|
21
|
+
path: ['billableServiceId'],
|
|
22
|
+
message: 'Select a billable service',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (value.sourceType === 'stockItem' && !value.stockItemId) {
|
|
27
|
+
context.addIssue({
|
|
28
|
+
code: z.ZodIssueCode.custom,
|
|
29
|
+
path: ['stockItemId'],
|
|
30
|
+
message: 'Select a stock item',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type EtimsItemMappingFormValues = z.infer<typeof etimsItemMappingFormSchema>;
|