@kenyaemr/esm-express-workflow-app 5.4.4-pre.299 → 5.4.4-pre.3

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 (124) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/dist/126.js +1 -0
  3. package/dist/126.js.map +1 -0
  4. package/dist/2024.js +1 -1
  5. package/dist/2024.js.map +1 -1
  6. package/dist/2225.js +1 -1
  7. package/dist/2225.js.map +1 -1
  8. package/dist/2386.js +1 -0
  9. package/dist/2386.js.map +1 -0
  10. package/dist/2438.js +1 -1
  11. package/dist/2438.js.map +1 -1
  12. package/dist/4504.js +1 -0
  13. package/dist/4504.js.map +1 -0
  14. package/dist/4670.js +1 -0
  15. package/dist/4670.js.map +1 -0
  16. package/dist/4682.js +1 -1
  17. package/dist/4682.js.map +1 -1
  18. package/dist/5047.js +1 -0
  19. package/dist/5047.js.map +1 -0
  20. package/dist/586.js.map +1 -1
  21. package/dist/5986.js +1 -1
  22. package/dist/5986.js.map +1 -1
  23. package/dist/6075.js +1 -0
  24. package/dist/6075.js.map +1 -0
  25. package/dist/6328.js +1 -1
  26. package/dist/6328.js.map +1 -1
  27. package/dist/64.js +1 -0
  28. package/dist/64.js.map +1 -0
  29. package/dist/6456.js +1 -1
  30. package/dist/6720.js +1 -0
  31. package/dist/6720.js.map +1 -0
  32. package/dist/7607.js +1 -1
  33. package/dist/8566.js +1 -0
  34. package/dist/8566.js.map +1 -0
  35. package/dist/8727.js +1 -1
  36. package/dist/8799.js +1 -0
  37. package/dist/8799.js.map +1 -0
  38. package/dist/8911.js +1 -1
  39. package/dist/8911.js.map +1 -1
  40. package/dist/9157.js +1 -1
  41. package/dist/9157.js.map +1 -1
  42. package/dist/9161.js +1 -0
  43. package/dist/9161.js.map +1 -0
  44. package/dist/9182.js +1 -1
  45. package/dist/9343.js +1 -0
  46. package/dist/9343.js.map +1 -0
  47. package/dist/9535.js +1 -1
  48. package/dist/9535.js.map +1 -1
  49. package/dist/9834.js +1 -0
  50. package/dist/{4432.js.map → 9834.js.map} +1 -1
  51. package/dist/kenyaemr-esm-express-workflow-app.js +2 -2
  52. package/dist/kenyaemr-esm-express-workflow-app.js.buildmanifest.json +294 -151
  53. package/dist/kenyaemr-esm-express-workflow-app.js.map +1 -1
  54. package/dist/main.js +2 -2
  55. package/dist/main.js.map +1 -1
  56. package/dist/routes.json +1 -1
  57. package/package.json +1 -1
  58. package/src/components/admissions/index.tsx +0 -1
  59. package/src/components/anaesthetic/anaesthetic.component.tsx +10 -4
  60. package/src/components/anaesthetic/forms/interoperative-record-drug-given-form.component.tsx +2 -26
  61. package/src/components/anaesthetic/graphs/interoperative-record-drug-given-graph-wrapper.component.tsx +66 -25
  62. package/src/components/anaesthetic/resources/anaesthetic.resource.ts +2 -2
  63. package/src/components/consultation/clinical-encounter/clinical-encounter.component.tsx +4 -20
  64. package/src/components/consultation/index.ts +1 -1
  65. package/src/components/consultation/tabs/index.ts +67 -0
  66. package/src/components/facility-dashboard/components/header/home-header.component.tsx +8 -13
  67. package/src/components/laboratory/index.ts +11 -0
  68. package/src/components/laboratory/lab-table.component.tsx +25 -12
  69. package/src/components/laboratory/laboratory-tabs.component.tsx +3 -17
  70. package/src/components/pharmacy/index.ts +1 -1
  71. package/src/components/pharmacy/pharmacy-tabs.component.tsx +4 -20
  72. package/src/components/pharmacy/tabs/index.ts +21 -0
  73. package/src/components/procedures/index.ts +16 -0
  74. package/src/components/procedures/procedures-table.component.tsx +33 -15
  75. package/src/components/procedures/procedures-tabs.component.tsx +3 -22
  76. package/src/components/radiology-and-imaging/index.ts +11 -0
  77. package/src/components/radiology-and-imaging/radiology-and-imaging-table.component.tsx +35 -15
  78. package/src/components/radiology-and-imaging/radiology-and-imaging.component.tsx +3 -19
  79. package/src/components/registration/card/Local-card/local-card.component.tsx +3 -3
  80. package/src/components/registration/dependants/dependants.component.tsx +6 -1
  81. package/src/components/registration/end-visit-modal/end-visit-dialog.modal.tsx +320 -0
  82. package/src/components/registration/end-visit-modal/end-visit-dialog.scss +12 -0
  83. package/src/components/registration/end-visit-modal/end-visit.resource.tsx +59 -0
  84. package/src/components/registration/end-visit-modal/sha-end-visit.resource.ts +285 -0
  85. package/src/components/registration/start-visit-form/overflow-menu-extension/custom-end-active-visit.extension.tsx +44 -0
  86. package/src/components/registration/start-visit-form/start-visit-workspace/exported-visit-form.workspace.tsx +16 -11
  87. package/src/components/registration/type/index.ts +101 -0
  88. package/src/config-schema.ts +23 -1
  89. package/src/index.ts +36 -17
  90. package/src/routes.json +205 -12
  91. package/src/shared/otp-verification/index.ts +3 -0
  92. package/src/shared/otp-verification/otp-verification.modal.tsx +51 -17
  93. package/src/shared/patient-chart/patient-summary-dashboard/patient-summary-dashboard.component.tsx +5 -73
  94. package/src/shared/tabs/create-tab-extension.tsx +33 -0
  95. package/src/shared/tabs/extension-tabs.component.tsx +51 -33
  96. package/src/shared/utils/index.ts +6 -6
  97. package/translations/am.json +32 -24
  98. package/translations/en.json +23 -16
  99. package/translations/fr.json +23 -16
  100. package/translations/sw.json +23 -21
  101. package/Failed to delete encounter/am.json +0 -4
  102. package/Failed to delete encounter/en.json +0 -4
  103. package/Failed to delete encounter/fr.json +0 -4
  104. package/Failed to delete encounter/sw.json +0 -4
  105. package/dist/2332.js +0 -1
  106. package/dist/2332.js.map +0 -1
  107. package/dist/4432.js +0 -1
  108. package/dist/4666.js +0 -1
  109. package/dist/4666.js.map +0 -1
  110. package/dist/5733.js +0 -1
  111. package/dist/5733.js.map +0 -1
  112. package/dist/644.js +0 -1
  113. package/dist/644.js.map +0 -1
  114. package/dist/655.js +0 -1
  115. package/dist/655.js.map +0 -1
  116. package/dist/70.js +0 -1
  117. package/dist/70.js.map +0 -1
  118. package/dist/9387.js +0 -1
  119. package/dist/9387.js.map +0 -1
  120. package/src/components/admissions/admission-medication-summary.tsx +0 -302
  121. package/src/components/admissions/admissions-dashboard.utils.test.ts +0 -48
  122. package/src/components/admissions/admissions-dashboard.utils.tsx +0 -63
  123. package/src/shared/orders/OrdersTabs.tsx +0 -76
  124. /package/src/shared/{patient-chart/patient-summary-dashboard → tabs}/patient-summary-dashboard.scss +0 -0
@@ -10,8 +10,6 @@ import {
10
10
  ModalHeader,
11
11
  NumberInput,
12
12
  TextArea,
13
- TextInput,
14
- IconButton,
15
13
  } from '@carbon/react';
16
14
  import {
17
15
  Phone,
@@ -21,8 +19,6 @@ import {
21
19
  RuleLocked,
22
20
  Renew,
23
21
  ChevronRight,
24
- WarningAlt,
25
- CheckmarkFilled,
26
22
  ArrowLeft,
27
23
  Time,
28
24
  } from '@carbon/react/icons';
@@ -83,6 +79,7 @@ type OTPVerificationModalProps = {
83
79
 
84
80
  authMode?: 'otp-only' | 'multi';
85
81
  whitelistedForOTP?: boolean;
82
+ facilityBiometricsEnforced?: boolean;
86
83
  onStartBiometric?: () => Promise<{
87
84
  embed_url: string;
88
85
  authorization_code: string;
@@ -120,6 +117,7 @@ type OTPVerificationModalProps = {
120
117
 
121
118
  onRejected?: () => void;
122
119
  ekycOrigin?: string;
120
+ visitAction?: 'start' | 'end';
123
121
  };
124
122
 
125
123
  const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
@@ -135,6 +133,7 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
135
133
  onCleanup,
136
134
  authMode = 'otp-only',
137
135
  whitelistedForOTP = false,
136
+ facilityBiometricsEnforced = false,
138
137
  onStartBiometric,
139
138
  onBiometricSuccess,
140
139
  onBiometricCancel,
@@ -146,8 +145,10 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
146
145
  onRequestWhitelist,
147
146
  onRejected,
148
147
  ekycOrigin,
148
+ visitAction = 'start',
149
149
  }) => {
150
150
  const { t } = useTranslation();
151
+ const canUseOtp = !facilityBiometricsEnforced || whitelistedForOTP;
151
152
  const [otp, setOtp] = useState('');
152
153
  const [newPhoneNumber, setNewPhoneNumber] = useState(phoneNumber);
153
154
  const [isLoading, setIsLoading] = useState(false);
@@ -208,6 +209,35 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
208
209
  }
209
210
  }, []);
210
211
 
212
+ // Reset mode and critical state when authMode or context changes (e.g., when modal is reused for different flows)
213
+ useEffect(() => {
214
+ const initialMode = authMode === 'multi' ? 'auth-landing' : 'landing';
215
+ setMode(initialMode);
216
+ setError(null);
217
+ setOtp('');
218
+ setFailedVerifyCount(0);
219
+
220
+ // Reset biometric state
221
+ setBiometricEmbedUrl(null);
222
+ setBiometricLoading(false);
223
+ setBiometricRetrying(false);
224
+ setBiometricFinalizing(false);
225
+ setBiometricPollElapsedSec(0);
226
+ biometricResultRef.current = null;
227
+
228
+ // Reset phone number and whitelist states
229
+ setNewPhoneNumber(phoneNumber);
230
+ setCurrentPhoneNumber(phoneNumber);
231
+ setWhitelistReasonCode('');
232
+ setWhitelistReasonText('');
233
+ setWhitelistBiometricAttempts(0);
234
+ setWhitelistAttachment(null);
235
+ setWhitelistSubmitting(false);
236
+
237
+ // Stop any active polling
238
+ stopBiometricPolling();
239
+ }, [authMode, phoneNumber, patientCRId, stopBiometricPolling]);
240
+
211
241
  useEffect(() => {
212
242
  return () => {
213
243
  onCleanup?.();
@@ -275,7 +305,6 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
275
305
  try {
276
306
  setRequestingOtp(true);
277
307
  await onRequestOtp?.(phone);
278
-
279
308
  setOtp('');
280
309
  setCurrentPhoneNumber(phone);
281
310
  setMode('verify-otp');
@@ -641,7 +670,7 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
641
670
  };
642
671
 
643
672
  const handleEscapeHatchFromVerifyOtp = () => {
644
- if (whitelistedForOTP) {
673
+ if (canUseOtp) {
645
674
  setOtp('');
646
675
  setIsCountdownActive(false);
647
676
  setMode('auth-landing');
@@ -730,7 +759,6 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
730
759
  review_type: r.review_type,
731
760
  requires_attachments: r.requires_attachments,
732
761
  }));
733
-
734
762
  return (
735
763
  <React.Fragment>
736
764
  <ModalHeader className={styles.sectionHeader} closeModal={handleClose}>
@@ -782,16 +810,16 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
782
810
  </button>
783
811
 
784
812
  <button
785
- className={`${styles.methodCard} ${!whitelistedForOTP ? styles.methodCardDisabled : ''}`}
813
+ className={`${styles.methodCard} ${!canUseOtp ? styles.methodCardDisabled : ''}`}
786
814
  onClick={handleSwitchToOtpFromAuthLanding}
787
- disabled={!whitelistedForOTP}>
815
+ disabled={!canUseOtp}>
788
816
  <div className={`${styles.methodIconWrap} ${styles.methodIconWarning}`}>
789
817
  <ChatBot size={22} />
790
818
  </div>
791
819
  <div className={styles.methodContent}>
792
820
  <div className={styles.methodTitleRow}>
793
821
  <p className={styles.methodTitle}>{t('btnUseOtp', 'Use OTP')}</p>
794
- {!whitelistedForOTP && (
822
+ {!canUseOtp && (
795
823
  <span className={styles.badgeRequired}>{t('whitelistingRequired', 'Whitelisting required')}</span>
796
824
  )}
797
825
  </div>
@@ -802,10 +830,10 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
802
830
  )}
803
831
  </p>
804
832
  </div>
805
- {whitelistedForOTP && <ChevronRight size={18} className={styles.methodArrow} />}
833
+ {canUseOtp && <ChevronRight size={18} className={styles.methodArrow} />}
806
834
  </button>
807
835
 
808
- {!whitelistedForOTP && (
836
+ {!canUseOtp && (
809
837
  <>
810
838
  <div className={styles.dividerWithText}>{t('or', 'or')}</div>
811
839
  <button
@@ -898,10 +926,12 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
898
926
  'otpFailedWhitelistedSubtitle',
899
927
  'Try biometrics instead — biometric capture is available for whitelisted patients.',
900
928
  )
901
- : t(
929
+ : facilityBiometricsEnforced
930
+ ? t(
902
931
  'otpFailedNotWhitelistedSubtitle',
903
932
  'Patient may not be receiving the SMS. Submit a whitelist request to bypass OTP for future visits.',
904
933
  )
934
+ : t('otpFailedRetrySubtitle', 'OTP failed multiple times. Please try again or use biometrics.')
905
935
  }
906
936
  />
907
937
  <Button
@@ -915,8 +945,10 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
915
945
  <InlineLoading description={t('checkingExistingRequest', 'Checking existing requests…')} />
916
946
  ) : whitelistedForOTP ? (
917
947
  t('switchToBiometric', 'Switch to biometric')
918
- ) : (
948
+ ) : facilityBiometricsEnforced ? (
919
949
  t('btnRequestOtpWhitelisting', 'Request OTP Whitelisting')
950
+ ) : (
951
+ t('switchToBiometric', 'Switch to biometric')
920
952
  )}
921
953
  </Button>
922
954
  </div>
@@ -940,7 +972,9 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
940
972
  <InlineLoading
941
973
  description={
942
974
  biometricFinalizing
943
- ? t('biometricCreatingVisit', 'Verified creating visit…')
975
+ ? visitAction === 'end'
976
+ ? t('biometricEndingVisit', 'Verified ending visit…')
977
+ : t('biometricCreatingVisit', 'Verified creating visit…')
944
978
  : t('biometricWaitingForApproval', 'Waiting for SHA to confirm capture…')
945
979
  }
946
980
  status="active"
@@ -961,7 +995,7 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
961
995
  kind="error"
962
996
  title={t('secureAuthBiometricFailedTitle', 'Biometric Verification Failed')}
963
997
  subtitle={
964
- whitelistedForOTP
998
+ canUseOtp
965
999
  ? t(
966
1000
  'secureAuthBiometricFailedBodyWhitelisted',
967
1001
  'Please try again or use OTP verification to continue.',
@@ -975,7 +1009,7 @@ const OTPVerificationModal: FC<OTPVerificationModalProps> = ({
975
1009
 
976
1010
  <p className={styles.chooseLabel}>{t('secureAuthChooseReason', 'Choose an option')}</p>
977
1011
 
978
- {whitelistedForOTP ? (
1012
+ {canUseOtp ? (
979
1013
  <button className={styles.methodCard} onClick={handleSwitchToOtpFromAuthLanding}>
980
1014
  <div className={`${styles.methodIconWrap} ${styles.methodIconWarning}`}>
981
1015
  <ChatBot size={22} />
@@ -1,80 +1,12 @@
1
- import { Column, Grid, Layer } from '@carbon/react';
2
- import {
3
- Activity,
4
- Attachment,
5
- Calendar,
6
- CloudMonitoring,
7
- Dashboard,
8
- IbmWatsonDiscovery,
9
- Settings,
10
- } from '@carbon/react/icons';
11
- import React, { useMemo } from 'react';
12
- import { useTranslation } from 'react-i18next';
13
- import ExtensionTabs, { ExtensionTabItem } from '../../tabs/extension-tabs.component';
14
- import styles from './patient-summary-dashboard.scss';
1
+ import React from 'react';
2
+ import ExtensionTabs from '../../tabs/extension-tabs.component';
15
3
 
16
4
  type PatientSummaryDashboardProps = {
17
5
  patientUuid: string;
18
- patient: fhir.Patient;
19
6
  };
20
-
21
- const PatientSummaryDashboard: React.FC<PatientSummaryDashboardProps> = ({ patientUuid, patient }) => {
22
- const { t } = useTranslation();
23
- const state = useMemo(() => ({ patientUuid, patient }), [patientUuid, patient]);
24
- const items: Array<ExtensionTabItem> = [
25
- {
26
- label: t('patientSummary', 'Patient Summary'),
27
- icon: Dashboard,
28
- slotName: 'ewf-patient-summary-slot',
29
- slotClassName: styles.ewfExtensionSlot,
30
- },
31
- {
32
- label: t('vitalsAndAnthropometric', 'Vitals & Anthropometric'),
33
- icon: Activity,
34
- slotName: 'ewf-vitals-slot',
35
- slotClassName: styles.ewfExtensionSlot,
36
- },
37
- {
38
- label: t('carePanel', 'Care Panel'),
39
- icon: CloudMonitoring,
40
- slotName: 'ewf-care-panel-slot',
41
- slotClassName: styles.ewfExtensionSlot,
42
- },
43
- {
44
- label: t('immunizations', 'Immunizations'),
45
- icon: IbmWatsonDiscovery,
46
- slotName: 'ewf-immunizations-slot',
47
- slotClassName: styles.ewfExtensionSlot,
48
- },
49
- {
50
- label: t('relationships', 'Relationships'),
51
- icon: Settings,
52
- slotName: 'ewf-relationships-slot',
53
- slotClassName: styles.ewfExtensionSlot,
54
- },
55
- {
56
- label: t('appointments', 'Appointments'),
57
- icon: Calendar,
58
- slotName: 'ewf-appointments-slot',
59
- slotClassName: styles.ewfExtensionSlot,
60
- },
61
- {
62
- label: t('attachments', 'Attachments'),
63
- icon: Attachment,
64
- slotName: 'ewf-attachments-slot',
65
- slotClassName: styles.ewfExtensionSlot,
66
- },
67
- ];
68
-
69
- return (
70
- <Layer>
71
- <Grid condensed>
72
- <Column lg={16} md={8} sm={4}>
73
- <ExtensionTabs items={items} state={state} />
74
- </Column>
75
- </Grid>
76
- </Layer>
77
- );
7
+ const PATIENT_CHART_PATIENT_SUMMARY_EXTENSION_SLOT = 'ewf-patient-chart-patient-summary-tabs-extension-slot';
8
+ const PatientSummaryDashboard: React.FC<PatientSummaryDashboardProps> = ({ patientUuid }) => {
9
+ return <ExtensionTabs extensionSlotName={PATIENT_CHART_PATIENT_SUMMARY_EXTENSION_SLOT} patientUuid={patientUuid} />;
78
10
  };
79
11
 
80
12
  export default PatientSummaryDashboard;
@@ -0,0 +1,33 @@
1
+ import { ExtensionSlot, FHIRResource } from '@openmrs/esm-framework';
2
+ import React from 'react';
3
+ import styles from './patient-summary-dashboard.scss';
4
+ export const createTabExtension = (extensionSlotName?: string) => {
5
+ return function TabExtension(props: any) {
6
+ return <TabWithExtensionSlot extensionSlotName={extensionSlotName} {...props} />;
7
+ };
8
+ };
9
+
10
+ const TabWithExtensionSlot: React.FC<{
11
+ extensionSlotName?: string;
12
+ patientUuid: string;
13
+ patient: FHIRResource;
14
+ _meta?: {
15
+ slot?: string;
16
+ [key: string]: any;
17
+ };
18
+ }> = ({ extensionSlotName, patientUuid, patient, _meta }) => {
19
+ if (!_meta?.slot && !extensionSlotName) {
20
+ console.warn(
21
+ 'No slot provided for TabExtension. Please provide a slot in the route meta or as an argument to createTabExtension.',
22
+ );
23
+ return null;
24
+ }
25
+ const { slot, ...others } = _meta || {};
26
+ return (
27
+ <ExtensionSlot
28
+ name={_meta?.slot ?? (extensionSlotName as string)}
29
+ state={{ patientUuid, patient, ...others }}
30
+ className={styles.ewfExtensionSlot}
31
+ />
32
+ );
33
+ };
@@ -1,41 +1,59 @@
1
- import React, { useState } from 'react';
2
- import { Tabs, TabList, Tab, TabPanels, TabPanel } from '@carbon/react';
3
- import { ExtensionSlot } from '@openmrs/esm-framework';
4
-
5
- export type ExtensionTabItem = {
6
- label: React.ReactNode;
7
- icon?: React.ComponentType<unknown>;
8
- slotName: string;
9
- slotClassName?: string;
10
- };
1
+ import { Layer, Tab, TabList, TabPanel, TabPanels, Tabs, TabsSkeleton } from '@carbon/react';
2
+ import { ErrorState, Extension, translateFrom, usePatient } from '@openmrs/esm-framework';
3
+ import { ComponentContext, useExtensionSlot } from '@openmrs/esm-framework/src/internal';
4
+ import React, { useMemo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
11
6
 
12
7
  type ExtensionTabsProps = {
13
- items: Array<ExtensionTabItem>;
14
- state?: Record<string, unknown>;
15
- contained?: boolean;
8
+ extensionSlotName: string;
9
+ patientUuid: string;
16
10
  };
17
11
 
18
- const ExtensionTabs: React.FC<ExtensionTabsProps> = ({ items, state, contained = true }) => {
19
- const [activeTab, setActiveTab] = useState<number>(0);
12
+ const ExtensionTabs: React.FC<ExtensionTabsProps> = ({ extensionSlotName, patientUuid }) => {
13
+ const { t } = useTranslation();
14
+ const { patient, isLoading, error } = usePatient(patientUuid);
15
+ const state = useMemo(() => ({ patientUuid, patient }), [patientUuid, patient]);
16
+
17
+ const { extensions, extensionSlotModuleName } = useExtensionSlot(extensionSlotName, state);
18
+ if (isLoading) {
19
+ return <TabsSkeleton />;
20
+ }
21
+
22
+ if (error) {
23
+ return <ErrorState headerTitle={t('error', 'Error')} error={error} />;
24
+ }
25
+
20
26
  return (
21
- <Tabs selectedIndex={activeTab} onChange={({ selectedIndex }) => setActiveTab(selectedIndex)}>
22
- <TabList contained={contained}>
23
- {items.map((item) => (
24
- <Tab key={item.slotName} renderIcon={item.icon}>
25
- {item.label}
26
- </Tab>
27
- ))}
28
- </TabList>
29
- <TabPanels>
30
- {items.map((item, index) => (
31
- <TabPanel key={item.slotName}>
32
- {index === activeTab ? (
33
- <ExtensionSlot className={item.slotClassName} name={item.slotName} state={state} />
34
- ) : null}
35
- </TabPanel>
36
- ))}
37
- </TabPanels>
38
- </Tabs>
27
+ <Layer>
28
+ <Tabs>
29
+ <TabList contained>
30
+ {extensions.map((ext) => (
31
+ <Tab key={ext.name}>{translateFrom(extensionSlotModuleName, ext.meta.title, ext.meta.title)}</Tab>
32
+ ))}
33
+ </TabList>
34
+ <TabPanels
35
+ data-extension-slot-name={extensionSlotName}
36
+ data-extension-slot-module-name={extensionSlotModuleName}>
37
+ {extensions.map((ext) => (
38
+ <TabPanel key={ext.name}>
39
+ <ComponentContext.Provider
40
+ key={ext.id}
41
+ value={{
42
+ featureName: ext.meta.featureName,
43
+ moduleName: ext.moduleName,
44
+ extension: {
45
+ extensionId: ext.id,
46
+ extensionSlotName: extensionSlotName,
47
+ extensionSlotModuleName: ext.moduleName,
48
+ },
49
+ }}>
50
+ <Extension state={state} />
51
+ </ComponentContext.Provider>
52
+ </TabPanel>
53
+ ))}
54
+ </TabPanels>
55
+ </Tabs>
56
+ </Layer>
39
57
  );
40
58
  };
41
59
 
@@ -187,7 +187,7 @@ export interface SavannahErrorResponse {
187
187
 
188
188
  /**
189
189
  * Returns the most specific human-readable error message available.
190
- * Priority: upstream_error.message > upstream_error.error > error > message > fallback
190
+ * Priority: upstream_error.message > upstream_error.error > message > error > fallback
191
191
  */
192
192
  export function extractUpstreamError(
193
193
  response: SavannahErrorResponse | string | unknown,
@@ -213,16 +213,16 @@ export function extractUpstreamError(
213
213
  return upstreamErr;
214
214
  }
215
215
 
216
- const topError = res.error?.trim();
217
- if (topError && !res.upstream_error) {
218
- return topError;
219
- }
220
-
221
216
  const topMsg = res.message?.trim();
222
217
  if (topMsg) {
223
218
  return topMsg;
224
219
  }
225
220
 
221
+ const topError = res.error?.trim();
222
+ if (topError && !res.upstream_error) {
223
+ return topError;
224
+ }
225
+
226
226
  if (topError) {
227
227
  return topError;
228
228
  }
@@ -2,7 +2,6 @@
2
2
  "accounting": "Accounting",
3
3
  "actions": "እርምጃዎች",
4
4
  "activeMedications": "አሁን ያሉ መድኃኒቶች",
5
- "activeMedicationsCount": "Active Medications",
6
5
  "activeVisitExists": "This patient already has an active visit",
7
6
  "add": "Add",
8
7
  "addAttachment": "Add attachment",
@@ -10,7 +9,6 @@
10
9
  "additionalVisitInformationUpdatedSuccessfully": "Additional visit information updated successfully",
11
10
  "addMedicationOrder": "Add Medication Order",
12
11
  "addOrder": "Add Order",
13
- "admissionRequest": "Admission request",
14
12
  "admissions": "ተኝተዋል",
15
13
  "Admitted/OPD": "የተኝተው/የውጪ ሕመምተኛ ጉብኝቶች",
16
14
  "all": "All",
@@ -35,13 +33,16 @@
35
33
  "awaitingConsultation": "Awaiting consultation",
36
34
  "awaitingProcedure": "Awaiting Procedure",
37
35
  "back": "Back",
36
+ "biometricAgentNotReachable": "Biometric agent not running. Please start the agent on this workstation and try again.",
38
37
  "biometricAttemptsHelper": "How many times biometric capture has been attempted (use 0 if not applicable).",
39
38
  "biometricAttemptsLabel": "Biometric attempts",
40
39
  "biometricCapture": "Biometric capture",
41
40
  "biometricCaptureTitle": "Biometric capture",
42
41
  "biometricCreatingVisit": "Verified creating visit…",
42
+ "biometricEndingVisit": "Verified ending visit…",
43
43
  "biometricError": "Biometric error",
44
44
  "biometricHint": "Follow the on-screen instructions in the capture window.",
45
+ "biometricMissingNationalId": "Provider National ID not configured. Please add it to your provider profile.",
45
46
  "biometricNoEmbedUrl": "Biometric session did not return an embed URL.",
46
47
  "biometricRejected": "Biometric verification was rejected. You can try again or use OTP.",
47
48
  "biometricRetrying": "Restarting…",
@@ -71,19 +72,22 @@
71
72
  "checkStatusNow": "Check status now",
72
73
  "chooseAnOption": "Choose an option",
73
74
  "chooseIdentifierType": "Choose identifier type",
74
- "clear": "Clear",
75
+ "claimProcessingError": "Error",
75
76
  "clearAll": "Clear All",
76
77
  "clearFilters": "Clear filters",
77
78
  "clientRegistry": "Client registry verification",
78
79
  "clientsPatientsAttendedTo": "Clients/Patients attended to",
79
80
  "clientsPatientsWaiting": "Clients/Patients waiting",
80
81
  "clinicalEncounter": "Clinical Encounter",
82
+ "clinicalEncounterTab": "የክሊኒካል ግኝት",
83
+ "clinicalNotes": "የክሊኒካል ማስታወሻዎች",
81
84
  "comingFrom": "Coming from",
82
85
  "completedProcedures": "Completed Procedures",
83
86
  "concentrationRate": "Concentration rate",
84
87
  "configureQueueRooms": "",
85
88
  "confirmationTxt": "Verify the phone number before OTP",
86
89
  "consultation": "Consultation",
90
+ "continue": "Continue",
87
91
  "continueWithCare": "Continue with care",
88
92
  "CRNumber": "CR Number",
89
93
  "customCheckin": "Check in",
@@ -101,9 +105,13 @@
101
105
  "diagnosisRequired": "Diagnosis is required",
102
106
  "diastolicBP": "BP Diastolic",
103
107
  "discard": "Discard",
108
+ "dischargeReason": "Discharge reason",
109
+ "dischargeReasonIntro": "Select the reason for ending this visit. You will then be asked to verify your identity.",
104
110
  "diseases": "በሽታዎች",
105
- "dose": "Dose",
106
- "drug": "Drug",
111
+ "drugOrderSaved": "Drug order saved",
112
+ "drugOrderSavedToBackend": "Drug order has been saved to backend",
113
+ "drugOrderSaveFailed": "Failed to save drug order",
114
+ "drugsIVFluids": "Drugs and IV Fluids Given",
107
115
  "editVisit": "Edit visit",
108
116
  "effect": "Effect",
109
117
  "emergency": "Emergency",
@@ -112,7 +120,9 @@
112
120
  "Encounter deleted successfully": "",
113
121
  "encounterDate": "Encounter Date",
114
122
  "encounterDetails": "Encounter Details",
123
+ "endActiveVisitConfirmation": "Are you sure you want to end this active visit?",
115
124
  "endActiveVisitFirst": "You must end the current visit before starting a new one.",
125
+ "endCustomActiveVisit": "End Active Visit",
116
126
  "endDate": "End date",
117
127
  "endDateTimeRequired": "End date and time are required",
118
128
  "ended": "Ended",
@@ -120,6 +130,8 @@
120
130
  "endTimeFormat": "End time format",
121
131
  "endTimeMustBeAfterMostRecentEncounter": "End time must be on or after {{lastEncounterDatetime}}",
122
132
  "endTimeMustBeAfterStartTime": "End time must be after start time",
133
+ "endVisit_title": "End Visit",
134
+ "endVisitTitle": "End Visit",
123
135
  "enterConcentrationRate": "Enter 1, 2.3, 3%, 4.5%",
124
136
  "enterFluidsGiven": "Enter fluids given",
125
137
  "enterIdentifierNumber": "Enter identifier number",
@@ -140,16 +152,13 @@
140
152
  "errorLoadingQueues": "Error loading queues",
141
153
  "errorUpdatingVisitAttribute": "Error updating the {{attributeName}} visit attribute",
142
154
  "errorUpdatingVisitDetails": "Error updating visit details",
143
- "failedToLoadDrugOrders": "Failed to load drug orders",
144
- "fieldRequired": "This field is required",
145
- "filters": "Filters",
146
- "fluidsAndParenteralNutritionSection": "የፈሳሾች እና የፓሬንቴራል አመጋገብ ክፍል",
147
155
  "estimatedBloodLoss": "Estimated Blood Loss (ML)",
148
156
  "estimatedBloodLossInvalid": "Estimated blood loss must be 0 or greater",
149
157
  "estimatedBloodLossRequired": "Estimated blood loss is required",
150
158
  "etco2": "EtCO2",
151
159
  "Failed to load membrane amniotic fluid data": "",
152
160
  "Failed to save membrane amniotic fluid data": "",
161
+ "failedToDeleteEncounter": "{{status}} {{statusText}}",
153
162
  "failedToSaveAnaestheticRecord": "Failed to save anaesthetic record",
154
163
  "failedToSaveDrugsIvFluids": "Failed to save drugs and IV fluids data",
155
164
  "failedToSavePartographyData": "Failed to save partography data: {{error}}",
@@ -176,20 +185,11 @@
176
185
  "immunizations": "Immunizations",
177
186
  "inductionAirway": "Induction (Airway)",
178
187
  "inPatient": "In-Patient",
179
- "inpatientDetail": "Inpatient Detail",
188
+ "insuranceClaimProcessingFailed": "Insurance claim processing failed",
180
189
  "interoperativeRecordDrugGiven": "Interoperative record",
181
190
  "inThePast": "In the past",
182
191
  "investigationAwaiting": "Investigation Awaiting",
183
192
  "investigationCompleted": "Investigation Completed",
184
- "administrationVolume": "መጠን",
185
- "administrationFrequency": "ድግግሞሽ",
186
- "administrationRate": "ፍጥነት",
187
- "administrationDuration": "ቆይታ",
188
- "administrationVolumeValue": "መጠን: {{value}}",
189
- "administrationFrequencyValue": "ድግግሞሽ: {{value}}",
190
- "administrationRateValue": "ፍጥነት: {{value}}",
191
- "administrationDurationValue": "ቆይታ: {{value}}",
192
- "itemAdministrationInstructions": "Item & Administration Instructions (Volume, Frequency, Rate, Duration)",
193
193
  "lab": "Lab",
194
194
  "labAwaiting": "Lab Awaiting",
195
195
  "labCompleted": "Lab Completed",
@@ -221,6 +221,7 @@
221
221
  "newPatientResults_other": "New patient(s) found ({{count}})",
222
222
  "nextPage": "Next page",
223
223
  "noAnaestheticRecordData": "No anaesthetic record data available",
224
+ "noCRNumber": "Patient has no SHA CR number.",
224
225
  "noDependentsFound": "No dependents found for this patient",
225
226
  "noEncounterTypeFound": "No encounter type found for graph: {{graphType}}",
226
227
  "noFilters": "No filters",
@@ -244,7 +245,6 @@
244
245
  "noValidObservations": "No valid observations to save",
245
246
  "noVisitTypesToDisplay": "No visit types to display",
246
247
  "numberofEmergencyCasesSeen": "የተታዩ የድንገተኛ ሁኔታ ሕመምተኞች ቁጥር",
247
- "nursingTreatmentSheet": "Nursing Treatment Sheet",
248
248
  "offlineVisit": "Offline Visit",
249
249
  "ongoing": "Ongoing",
250
250
  "ongoingProcedures": "Ongoing Procedures",
@@ -263,6 +263,7 @@
263
263
  "otpExpiryInfo": "The OTP will be valid for {{minutes}} minutes after it is sent.",
264
264
  "otpFailedNotWhitelistedSubtitle": "Patient may not be receiving the SMS. Submit a whitelist request to bypass OTP for future visits.",
265
265
  "otpFailedRepeatedly": "OTP failed 3 times",
266
+ "otpFailedRetrySubtitle": "OTP failed multiple times. Please try again or use biometrics.",
266
267
  "otpFailedWhitelisted": "OTP not working?",
267
268
  "otpFailedWhitelistedSubtitle": "Try biometrics instead — biometric capture is available for whitelisted patients.",
268
269
  "otpRequestError": "Error requesting OTP",
@@ -272,15 +273,16 @@
272
273
  "otpVerificationError": "Error verifying OTP",
273
274
  "otpVerificationFailed": "OTP verification failed. Please try again.",
274
275
  "partOfFormDidntLoad": "Part of the form did not load",
276
+ "partographyDataSavedSuccessfully": "Partography data saved successfully",
275
277
  "pastMedications": "ያለፉ መድኃኒቶች",
276
- "pastMedicationsCount": "Past Medications",
277
278
  "patientAttended": "ሕመምተኛ ተንከባክቧል",
278
279
  "patientCategory": "የሕመምተኛ ምድብ",
279
280
  "patientExamination": "የሕመምተኛ ምርመራ",
280
281
  "patientHistory": "የሕመምተኛ ታሪክ",
281
282
  "patientManagement": "የሕመምተኛ አስተዳደር",
283
+ "patientMissingUuid": "Patient is missing a UUID",
282
284
  "patientsAwaiting": "የሚጠብቅ ሕመምተኛ",
283
- "patientSummary": "Patient Summary",
285
+ "patientSummaryTab": "የታካሚ ማጠቃለያ",
284
286
  "pendingMedicationOrders": "Pending Medication Orders",
285
287
  "pharmacy": "Pharmacy",
286
288
  "pharmacyOrdersDescription": "Active medication prescriptions awaiting pharmacist review and dispensing",
@@ -298,6 +300,7 @@
298
300
  "priority": "Priority",
299
301
  "priorityComment": "Priority Comment",
300
302
  "procedures": "Procedures",
303
+ "proceduresAndOrders": "ሂደቶች እና ትዕዛዞች",
301
304
  "proceduresAwaiting": "Procedures Awaiting",
302
305
  "proceduresCompleted": "Procedures Completed",
303
306
  "proceduresOrders": "Procedures Orders",
@@ -349,7 +352,6 @@
349
352
  "searchForAPatient": "Search for a patient",
350
353
  "searchForAVisitType": "Search for a visit type",
351
354
  "searching": "Searching...",
352
- "searchingDiagnosis": "Searching diagnosis...",
353
355
  "searchMedicationOrders": "Search medication orders",
354
356
  "searchPatients": "Search for Patient(s)",
355
357
  "searchPlaceholder": "Type to search (minimum 3 characters)...",
@@ -367,6 +369,7 @@
367
369
  "secureAuthOtpOption": "One-Time Password (Whitelisting is a must) sent to your registered phone number or email",
368
370
  "secureAuthTitle": "Secure Patient Authentication",
369
371
  "selectAnOption": "Select an option",
372
+ "selectDischargeReason": "Select a reason",
370
373
  "selectIdentifierAndNumber": "Please select an identifier type and enter an identifier number",
371
374
  "selectLocation": "Select a location",
372
375
  "selectOrSearchDiagnosis": "Select from list or type at least 3 letters to search ICD-11 diagnosis",
@@ -401,6 +404,9 @@
401
404
  "tableView": "Table View",
402
405
  "theVisitIs": "The visit is",
403
406
  "time": "Time",
407
+ "Time": "",
408
+ "timeGiven": "Time given",
409
+ "timeGivenRequired": "Time given is required",
404
410
  "top10DiseasesOver5": "ከ5 ዓመት በላይ ዕድሜ ያላቸው ከፍተኛ 10 በሽታዎች",
405
411
  "top10DiseasesUnder5": "ከ5 ዓመት በታች ዕድሜ ያላቸው ከፍተኛ 10 በሽታዎች",
406
412
  "totalNumberofReferrals-IN": "አጠቃላይ የመላኪያ ቁጥር - ወደ ውስጥ",
@@ -437,6 +443,7 @@
437
443
  "visitDetails": "Visit Details",
438
444
  "visitDetailsUpdated": "Visit details updated",
439
445
  "visitDetailsUpdatedSuccessfully": "{{visit}} updated successfully",
446
+ "visitEnded": "Visit ended",
440
447
  "visitLocation": "Visit Location",
441
448
  "visitLocationRequired": "Visit location is required",
442
449
  "visitProvider": "Visit Provider",
@@ -480,5 +487,6 @@
480
487
  "whitelistStillPendingShort": "Request is still pending review. We will keep checking automatically.",
481
488
  "whitelistSubmitFailed": "Whitelist submission failed.",
482
489
  "whitelistWaitingTitle": "Waiting for whitelist approval",
483
- "yearsOld": "years old"
490
+ "yearsOld": "years old",
491
+ "youWillProcessTheClaim": "You will process the insurance claim for this visit."
484
492
  }