@digihmis/esm-outpatient-app 1.0.0

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 (116) hide show
  1. package/README.md +4 -0
  2. package/dist/117.js +1 -0
  3. package/dist/117.js.map +1 -0
  4. package/dist/132.js +1 -0
  5. package/dist/132.js.map +1 -0
  6. package/dist/152.js +1 -0
  7. package/dist/152.js.map +1 -0
  8. package/dist/209.js +1 -0
  9. package/dist/209.js.map +1 -0
  10. package/dist/317.js +1 -0
  11. package/dist/317.js.map +1 -0
  12. package/dist/349.js +1 -0
  13. package/dist/349.js.map +1 -0
  14. package/dist/371.js +1 -0
  15. package/dist/371.js.map +1 -0
  16. package/dist/378.js +1 -0
  17. package/dist/378.js.map +1 -0
  18. package/dist/395.js +12 -0
  19. package/dist/395.js.map +1 -0
  20. package/dist/442.js +1 -0
  21. package/dist/442.js.map +1 -0
  22. package/dist/45.js +1 -0
  23. package/dist/45.js.map +1 -0
  24. package/dist/466.js +1 -0
  25. package/dist/466.js.map +1 -0
  26. package/dist/508.js +1 -0
  27. package/dist/508.js.map +1 -0
  28. package/dist/564.js +1 -0
  29. package/dist/564.js.map +1 -0
  30. package/dist/61.js +1 -0
  31. package/dist/61.js.map +1 -0
  32. package/dist/640.js +1 -0
  33. package/dist/640.js.map +1 -0
  34. package/dist/689.js +1 -0
  35. package/dist/689.js.map +1 -0
  36. package/dist/709.js +1 -0
  37. package/dist/709.js.map +1 -0
  38. package/dist/712.js +1 -0
  39. package/dist/712.js.map +1 -0
  40. package/dist/720.js +1 -0
  41. package/dist/720.js.map +1 -0
  42. package/dist/742.js +1 -0
  43. package/dist/742.js.map +1 -0
  44. package/dist/759.js +15 -0
  45. package/dist/759.js.map +1 -0
  46. package/dist/771.js +1 -0
  47. package/dist/771.js.map +1 -0
  48. package/dist/797.js +1 -0
  49. package/dist/797.js.map +1 -0
  50. package/dist/806.js +1 -0
  51. package/dist/806.js.map +1 -0
  52. package/dist/912.js +1 -0
  53. package/dist/912.js.map +1 -0
  54. package/dist/913.js +1 -0
  55. package/dist/913.js.map +1 -0
  56. package/dist/940.js +1 -0
  57. package/dist/940.js.map +1 -0
  58. package/dist/974.js +1 -0
  59. package/dist/974.js.map +1 -0
  60. package/dist/digihmis-esm-outpatient-app.js +6 -0
  61. package/dist/digihmis-esm-outpatient-app.js.buildmanifest.json +827 -0
  62. package/dist/digihmis-esm-outpatient-app.js.map +1 -0
  63. package/dist/main.js +17 -0
  64. package/dist/main.js.map +1 -0
  65. package/dist/routes.json +1 -0
  66. package/jest.config.js +26 -0
  67. package/package.json +51 -0
  68. package/rspack.config.js +1 -0
  69. package/src/config-schema.ts +347 -0
  70. package/src/dashboard.meta.ts +16 -0
  71. package/src/declarations.d.ts +6 -0
  72. package/src/icon-wrapper/icon-wrapper.extension.tsx +17 -0
  73. package/src/index.ts +27 -0
  74. package/src/left-panel/left-panel.component.tsx +100 -0
  75. package/src/left-panel/left-panel.scss +13 -0
  76. package/src/main-dashboard/outpatient-dashboard.component.tsx +264 -0
  77. package/src/main-dashboard/outpatient-dashboard.resource.ts +71 -0
  78. package/src/main-dashboard/outpatient-dashboard.scss +166 -0
  79. package/src/main-dashboard/type/index.ts +162 -0
  80. package/src/page-header/page-header.component.tsx +35 -0
  81. package/src/page-header/page-header.scss +76 -0
  82. package/src/patient-management/components/auto-suggest/autosuggest.component.tsx +149 -0
  83. package/src/patient-management/components/auto-suggest/autosuggest.scss +61 -0
  84. package/src/patient-management/components/auto-suggest/diagnosis-autosuggest.component.tsx +131 -0
  85. package/src/patient-management/components/auto-suggest/diagnosis-autosuggest.scss +42 -0
  86. package/src/patient-management/patient-banner/patient-banner.component.tsx +60 -0
  87. package/src/patient-management/patient-banner/patient-banner.scss +72 -0
  88. package/src/patient-management/patient-management-workspace/billable-services-selector.component.tsx +150 -0
  89. package/src/patient-management/patient-management-workspace/patient-management-schema/index.ts +261 -0
  90. package/src/patient-management/patient-management-workspace/patient-management-workspace.scss +304 -0
  91. package/src/patient-management/patient-management-workspace/patient-management-workspace.tsx +1581 -0
  92. package/src/patient-management/patient-management-workspace/patient-management.resource.ts +182 -0
  93. package/src/patient-management/patient-management-workspace/type/index.ts +193 -0
  94. package/src/patient-management/patient-management-workspace/utils/index.ts +172 -0
  95. package/src/patient-management/patient-management.component.tsx +97 -0
  96. package/src/patient-management/patient-management.scss +30 -0
  97. package/src/patient-management/utils/index.ts +5 -0
  98. package/src/queue-room-table/queue-room-table.component.tsx +313 -0
  99. package/src/queue-room-table/queue-room-table.resource.ts +73 -0
  100. package/src/queue-room-table/queue-room-table.scss +193 -0
  101. package/src/queue-room-table/type/index.ts +34 -0
  102. package/src/queue-room-table/useQueueActions.ts +24 -0
  103. package/src/routes.json +55 -0
  104. package/src/setup-tests.ts +1 -0
  105. package/translations/am.json +57 -0
  106. package/translations/ar.json +57 -0
  107. package/translations/en.json +57 -0
  108. package/translations/es.json +57 -0
  109. package/translations/fr.json +57 -0
  110. package/translations/he.json +57 -0
  111. package/translations/km.json +57 -0
  112. package/translations/pt.json +57 -0
  113. package/translations/sw.json +57 -0
  114. package/translations/vi.json +57 -0
  115. package/translations/zh.json +57 -0
  116. package/tsconfig.json +5 -0
@@ -0,0 +1,1581 @@
1
+ import React, { useState, useMemo, useEffect, useCallback } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { useForm, Controller } from 'react-hook-form';
4
+ import { zodResolver } from '@hookform/resolvers/zod';
5
+ import {
6
+ Form,
7
+ ButtonSet,
8
+ Button,
9
+ Stack,
10
+ TextArea,
11
+ RadioButtonGroup,
12
+ RadioButton,
13
+ Tag,
14
+ TreeView,
15
+ InlineNotification,
16
+ InlineLoading,
17
+ TreeNode,
18
+ Layer,
19
+ FilterableMultiSelect,
20
+ } from '@carbon/react';
21
+ import { type DefaultWorkspaceProps, launchWorkspace, ResponsiveWrapper, showModal, useConfig } from '@openmrs/esm-framework';
22
+ import capitalize from 'lodash-es/capitalize';
23
+ import {
24
+ useBillableServicesWithPricing,
25
+ useConceptAnswers,
26
+ useDiagnosis,
27
+ usePatientPaymentType,
28
+ usePerson,
29
+ } from './patient-management.resource';
30
+ import {
31
+ ClinicalEncounterSchema,
32
+ ClinicalEncounterFormData,
33
+ OTHER_SYMPTOM_UUID,
34
+ SURGICAL_HISTORY_UUID,
35
+ ADMISSION_HISTORY_UUID,
36
+ OTHER_PAST_ILLNESS_UUID,
37
+ YES_ANSWER_UUID,
38
+ ABNORMAL_GAIT_UUID,
39
+ JAUNDICE_UUID,
40
+ CYANOSIS_UUID,
41
+ DEHYDRATION_UUID,
42
+ FINGER_CLUBBING_UUID,
43
+ OEDEMA_UUID,
44
+ PALLOR_UUID,
45
+ LYMPHADENOPATHY_UUID,
46
+ GenericItem,
47
+ ProcedureWithBilling,
48
+ TransformedBillableItem,
49
+ } from './patient-management-schema';
50
+ import { ConfigObject } from '../../config-schema';
51
+ import styles from './patient-management-workspace.scss';
52
+ import debounce from 'lodash-es/debounce';
53
+ import { DiagnosisAutosuggest } from '../components/auto-suggest/diagnosis-autosuggest.component';
54
+ import { useCurrencyFormatting } from './utils';
55
+ import { BillableServicesSelector } from './billable-services-selector.component';
56
+
57
+ interface ClinicalEncounterWorkspaceProps extends DefaultWorkspaceProps {
58
+ patientUuid: string;
59
+ formContext: 'creating' | 'editing';
60
+ editEncounterUuid?: string;
61
+ queueEntryUuid?: string;
62
+ completeServiceCallback?: () => Promise<void>;
63
+ }
64
+
65
+ const ClinicalEncounterWorkspace: React.FC<ClinicalEncounterWorkspaceProps> = ({ closeWorkspace, patientUuid }) => {
66
+ const { t } = useTranslation();
67
+ const config = useConfig<ConfigObject>();
68
+ const [currentSection, setCurrentSection] = useState(0);
69
+
70
+ const procedureServices = useBillableServicesWithPricing(config.SurgicalOutpatientDepartmentServiceConceptUuid);
71
+ const labServices = useBillableServicesWithPricing(config.LaboratoryServiceConceptUuid);
72
+ const imagingServices = useBillableServicesWithPricing(config.ImagingServiceConceptUuid);
73
+
74
+ const paymentInfo = usePatientPaymentType(patientUuid);
75
+
76
+ const procedureOptionsWithPrices = useMemo(
77
+ () => procedureServices.transformItems(procedureServices.lineItems, paymentInfo.isInsurance),
78
+ [procedureServices, paymentInfo.isInsurance],
79
+ );
80
+
81
+ const labOptionsWithPrices = useMemo(
82
+ () => labServices.transformItems(labServices.lineItems, paymentInfo.isInsurance),
83
+ [labServices, paymentInfo.isInsurance],
84
+ );
85
+
86
+ const imagingOptionsWithPrices = useMemo(
87
+ () => imagingServices.transformItems(imagingServices.lineItems, paymentInfo.isInsurance),
88
+ [imagingServices, paymentInfo.isInsurance],
89
+ );
90
+
91
+ const {
92
+ control,
93
+ handleSubmit: handleFormSubmit,
94
+ watch,
95
+ setValue,
96
+ trigger,
97
+ getValues,
98
+ formState: { errors, isSubmitting },
99
+ } = useForm<ClinicalEncounterFormData>({
100
+ resolver: zodResolver(ClinicalEncounterSchema),
101
+ defaultValues: {
102
+ symptoms: [] as Array<GenericItem>,
103
+ pastIllness: [] as Array<GenericItem>,
104
+ onsetStatus: '',
105
+ tbContact: '',
106
+ otherSymptomText: '',
107
+ surgeryDetails: '',
108
+ admissionReason: '',
109
+ otherPastIllnessText: '',
110
+ medicationsTaken: '',
111
+ medicationDetails: '',
112
+ // Physical Examination defaults
113
+ generalExamination: [] as Array<GenericItem>,
114
+ generalExaminationNotes: '',
115
+ gaitFinding: '',
116
+ jaundiceGrading: '',
117
+ cyanosisType: '',
118
+ dehydrationType: [] as Array<GenericItem>,
119
+ fingerclubGrading: '',
120
+ oedemaLocation: [] as Array<GenericItem>,
121
+ oedemaType: '',
122
+ oedemaGrading: '',
123
+ pallorLocation: [] as Array<GenericItem>,
124
+ pallorGrading: '',
125
+ siteLymphadenopathy: [] as Array<GenericItem>,
126
+ // Diagnosis defaults
127
+ primaryDiagnosis: [] as Array<GenericItem>,
128
+ secondaryDiagnosis: [] as Array<GenericItem>,
129
+ diagnosisNotes: '',
130
+ procedures: [] as Array<TransformedBillableItem>,
131
+ labTests: [] as Array<TransformedBillableItem>,
132
+ imagingTests: [] as Array<TransformedBillableItem>,
133
+ notes: '',
134
+ patientOutcome: '',
135
+ },
136
+ mode: 'onTouched',
137
+ });
138
+
139
+ // Watch form values
140
+ const symptoms = watch('symptoms');
141
+ const pastIllness = watch('pastIllness');
142
+ const medicationsTaken = watch('medicationsTaken');
143
+ const generalExamination = watch('generalExamination');
144
+
145
+ // Fetch person data
146
+ const { person } = usePerson(patientUuid);
147
+ const openmrsId =
148
+ person?.identifiers?.find((id) => id.identifierType?.uuid === config.OpenmrsIdentifierTypeUuid)?.identifier || '';
149
+
150
+ // Fetch TB contact answers
151
+ const { data: tbContactAnswersRaw, isLoading: isLoadingTbAnswers } = useConceptAnswers(
152
+ config.tuberculosisContactQuestionConceptUuid,
153
+ );
154
+
155
+ const tbContactAnswers = useMemo(
156
+ () => tbContactAnswersRaw?.filter((answer) => answer.uuid !== '1067AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') || [],
157
+ [tbContactAnswersRaw],
158
+ );
159
+
160
+ // Prepare options for Section 0 & 1
161
+ const symptomOptions = useMemo(
162
+ () =>
163
+ (config.symptomsConceptAnswers || []).map((symptom) => ({
164
+ id: symptom.concept,
165
+ text: symptom.label,
166
+ })),
167
+ [config.symptomsConceptAnswers],
168
+ );
169
+
170
+ const pastIllnessOptions = useMemo(
171
+ () =>
172
+ (config.pastIllnessAnswers || []).map((pastIllness) => ({
173
+ id: pastIllness.concept,
174
+ text: pastIllness.label,
175
+ })),
176
+ [config.pastIllnessAnswers],
177
+ );
178
+
179
+ const onsetStatusAnswers = useMemo(
180
+ () =>
181
+ (config.onsetStatusAnswers || []).map((answer) => ({
182
+ uuid: answer.concept,
183
+ label: answer.label,
184
+ })),
185
+ [config.onsetStatusAnswers],
186
+ );
187
+
188
+ // Prepare options for Section 2 - Physical Examination
189
+ const generalExaminationOptions = useMemo(
190
+ () =>
191
+ (config.generalExaminationConceptAnswers || []).map((exam) => ({
192
+ id: exam.concept,
193
+ text: exam.label,
194
+ })),
195
+ [config.generalExaminationConceptAnswers],
196
+ );
197
+
198
+ const gaitFindingOptions = useMemo(
199
+ () =>
200
+ (config.gaitFindingConceptAnswers || []).map((answer) => ({
201
+ uuid: answer.concept,
202
+ label: answer.label,
203
+ })),
204
+ [config.gaitFindingConceptAnswers],
205
+ );
206
+
207
+ const jaundiceGradingOptions = useMemo(
208
+ () =>
209
+ (config.jaundiceGradingAnswers || []).map((answer) => ({
210
+ uuid: answer.concept,
211
+ label: answer.label,
212
+ })),
213
+ [config.jaundiceGradingAnswers],
214
+ );
215
+
216
+ const cyanosisTypeOptions = useMemo(
217
+ () =>
218
+ (config.cyanosisTypeAnswers || []).map((answer) => ({
219
+ uuid: answer.concept,
220
+ label: answer.label,
221
+ })),
222
+ [config.cyanosisTypeAnswers],
223
+ );
224
+
225
+ const dehydrationTypeOptions = useMemo(
226
+ () =>
227
+ (config.dehydrationTypeAnswers || []).map((answer) => ({
228
+ id: answer.concept,
229
+ text: answer.label,
230
+ })),
231
+ [config.dehydrationTypeAnswers],
232
+ );
233
+
234
+ const fingerclubGradingOptions = useMemo(
235
+ () =>
236
+ (config.fingerclubGradingAnswers || []).map((answer) => ({
237
+ uuid: answer.concept,
238
+ label: answer.label,
239
+ })),
240
+ [config.fingerclubGradingAnswers],
241
+ );
242
+
243
+ const oedemaLocationOptions = useMemo(
244
+ () =>
245
+ (config.oedemaLocationAnswers || []).map((answer) => ({
246
+ id: answer.concept,
247
+ text: answer.label,
248
+ })),
249
+ [config.oedemaLocationAnswers],
250
+ );
251
+
252
+ const oedemaTypeOptions = useMemo(
253
+ () =>
254
+ (config.oedemaTypeAnswers || []).map((answer) => ({
255
+ uuid: answer.concept,
256
+ label: answer.label,
257
+ })),
258
+ [config.oedemaTypeAnswers],
259
+ );
260
+
261
+ const oedemaGradingOptions = useMemo(
262
+ () =>
263
+ (config.oedemaGradingAnswers || []).map((answer) => ({
264
+ uuid: answer.concept,
265
+ label: answer.label,
266
+ })),
267
+ [config.oedemaGradingAnswers],
268
+ );
269
+
270
+ const pallorLocationOptions = useMemo(
271
+ () =>
272
+ (config.pallorLocationAnswers || []).map((answer) => ({
273
+ id: answer.concept,
274
+ text: answer.label,
275
+ })),
276
+ [config.pallorLocationAnswers],
277
+ );
278
+
279
+ const pallorGradingOptions = useMemo(
280
+ () =>
281
+ (config.pallorGradingAnswers || []).map((answer) => ({
282
+ uuid: answer.concept,
283
+ label: answer.label,
284
+ })),
285
+ [config.pallorGradingAnswers],
286
+ );
287
+
288
+ const patientOutcomeOptions = useMemo(
289
+ () =>
290
+ (config.patientOutcomeAnswers || []).map((answer) => ({
291
+ uuid: answer.concept,
292
+ label: answer.label,
293
+ })),
294
+ [config.patientOutcomeAnswers],
295
+ );
296
+
297
+ const siteLymphadenopathyOptions = useMemo(
298
+ () =>
299
+ (config.siteLymphadenopathyAnswers || []).map((answer) => ({
300
+ id: answer.concept,
301
+ text: answer.label,
302
+ })),
303
+ [config.siteLymphadenopathyAnswers],
304
+ );
305
+
306
+ // Sections
307
+ const sections = [
308
+ { id: 0, label: t('symptoms', 'Symptoms') },
309
+ { id: 1, label: t('presentIllness', 'History of present illness') },
310
+ { id: 2, label: t('physicalExamination', 'Physical examination') },
311
+ { id: 3, label: t('diagnosis', 'Diagnosis') },
312
+ { id: 4, label: t('procedures', 'Procedures') },
313
+ { id: 5, label: t('laboratory', 'Laboratory') },
314
+ { id: 6, label: t('imaging', 'Imaging') },
315
+ { id: 7, label: t('notesInstructions', 'Notes/Instructions') },
316
+ { id: 8, label: t('outcome', 'Outcome') },
317
+ ];
318
+ // Computed values for Section 0
319
+ const hasCoughSymptom = useMemo(
320
+ () => symptoms?.some((symptom) => symptom.id === config.coughConceptUuid) || false,
321
+ [symptoms, config.coughConceptUuid],
322
+ );
323
+
324
+ const hasOtherSymptom = useMemo(
325
+ () => symptoms?.some((symptom) => symptom.id === OTHER_SYMPTOM_UUID) || false,
326
+ [symptoms],
327
+ );
328
+
329
+ // Computed values for Section 1
330
+ const hasSurgicalHistory = useMemo(
331
+ () => pastIllness?.some((illness) => illness.id === SURGICAL_HISTORY_UUID) || false,
332
+ [pastIllness],
333
+ );
334
+
335
+ const hasAdmissionHistory = useMemo(
336
+ () => pastIllness?.some((illness) => illness.id === ADMISSION_HISTORY_UUID) || false,
337
+ [pastIllness],
338
+ );
339
+
340
+ const hasOtherPastIllness = useMemo(
341
+ () => pastIllness?.some((illness) => illness.id === OTHER_PAST_ILLNESS_UUID) || false,
342
+ [pastIllness],
343
+ );
344
+
345
+ // Computed values for Section 2 - Physical Examination
346
+ const hasAbnormalGait = useMemo(
347
+ () => generalExamination?.some((exam) => exam.id === ABNORMAL_GAIT_UUID) || false,
348
+ [generalExamination],
349
+ );
350
+
351
+ const hasJaundice = useMemo(
352
+ () => generalExamination?.some((exam) => exam.id === JAUNDICE_UUID) || false,
353
+ [generalExamination],
354
+ );
355
+
356
+ const hasCyanosis = useMemo(
357
+ () => generalExamination?.some((exam) => exam.id === CYANOSIS_UUID) || false,
358
+ [generalExamination],
359
+ );
360
+
361
+ const hasDehydration = useMemo(
362
+ () => generalExamination?.some((exam) => exam.id === DEHYDRATION_UUID) || false,
363
+ [generalExamination],
364
+ );
365
+
366
+ const hasFingerClubbing = useMemo(
367
+ () => generalExamination?.some((exam) => exam.id === FINGER_CLUBBING_UUID) || false,
368
+ [generalExamination],
369
+ );
370
+
371
+ const hasOedema = useMemo(
372
+ () => generalExamination?.some((exam) => exam.id === OEDEMA_UUID) || false,
373
+ [generalExamination],
374
+ );
375
+
376
+ const hasPallor = useMemo(
377
+ () => generalExamination?.some((exam) => exam.id === PALLOR_UUID) || false,
378
+ [generalExamination],
379
+ );
380
+
381
+ const hasLymphadenopathy = useMemo(
382
+ () => generalExamination?.some((exam) => exam.id === LYMPHADENOPATHY_UUID) || false,
383
+ [generalExamination],
384
+ );
385
+
386
+ // Clear dependent fields for Section 0
387
+ useEffect(() => {
388
+ if (!hasCoughSymptom && getValues('tbContact')) {
389
+ setValue('tbContact', '');
390
+ }
391
+ }, [hasCoughSymptom, setValue, getValues]);
392
+
393
+ useEffect(() => {
394
+ if (!hasOtherSymptom && getValues('otherSymptomText')) {
395
+ setValue('otherSymptomText', '');
396
+ }
397
+ }, [hasOtherSymptom, setValue, getValues]);
398
+
399
+ useEffect(() => {
400
+ if ((!symptoms || symptoms.length === 0) && getValues('onsetStatus')) {
401
+ setValue('onsetStatus', '');
402
+ }
403
+ }, [symptoms, setValue, getValues]);
404
+
405
+ // Clear dependent fields for Section 1
406
+ useEffect(() => {
407
+ if (!hasSurgicalHistory && getValues('surgeryDetails')) {
408
+ setValue('surgeryDetails', '');
409
+ }
410
+ }, [hasSurgicalHistory, setValue, getValues]);
411
+
412
+ useEffect(() => {
413
+ if (!hasAdmissionHistory && getValues('admissionReason')) {
414
+ setValue('admissionReason', '');
415
+ }
416
+ }, [hasAdmissionHistory, setValue, getValues]);
417
+
418
+ useEffect(() => {
419
+ if (!hasOtherPastIllness && getValues('otherPastIllnessText')) {
420
+ setValue('otherPastIllnessText', '');
421
+ }
422
+ }, [hasOtherPastIllness, setValue, getValues]);
423
+
424
+ useEffect(() => {
425
+ if (medicationsTaken !== YES_ANSWER_UUID && getValues('medicationDetails')) {
426
+ setValue('medicationDetails', '');
427
+ }
428
+ }, [medicationsTaken, setValue, getValues]);
429
+
430
+ // Clear dependent fields for Section 2 - Physical Examination
431
+ useEffect(() => {
432
+ if (!hasAbnormalGait && getValues('gaitFinding')) {
433
+ setValue('gaitFinding', '');
434
+ }
435
+ }, [hasAbnormalGait, setValue, getValues]);
436
+
437
+ useEffect(() => {
438
+ if (!hasJaundice && getValues('jaundiceGrading')) {
439
+ setValue('jaundiceGrading', '');
440
+ }
441
+ }, [hasJaundice, setValue, getValues]);
442
+
443
+ useEffect(() => {
444
+ if (!hasCyanosis && getValues('cyanosisType')) {
445
+ setValue('cyanosisType', '');
446
+ }
447
+ }, [hasCyanosis, setValue, getValues]);
448
+
449
+ useEffect(() => {
450
+ if (!hasDehydration && getValues('dehydrationType')?.length) {
451
+ setValue('dehydrationType', []);
452
+ }
453
+ }, [hasDehydration, setValue, getValues]);
454
+
455
+ useEffect(() => {
456
+ if (!hasFingerClubbing && getValues('fingerclubGrading')) {
457
+ setValue('fingerclubGrading', '');
458
+ }
459
+ }, [hasFingerClubbing, setValue, getValues]);
460
+
461
+ useEffect(() => {
462
+ if (!hasOedema) {
463
+ if (getValues('oedemaLocation')?.length) {
464
+ setValue('oedemaLocation', []);
465
+ }
466
+ if (getValues('oedemaType')) {
467
+ setValue('oedemaType', '');
468
+ }
469
+ if (getValues('oedemaGrading')) {
470
+ setValue('oedemaGrading', '');
471
+ }
472
+ }
473
+ }, [hasOedema, setValue, getValues]);
474
+
475
+ useEffect(() => {
476
+ if (!hasPallor) {
477
+ if (getValues('pallorLocation')?.length) {
478
+ setValue('pallorLocation', []);
479
+ }
480
+ if (getValues('pallorGrading')) {
481
+ setValue('pallorGrading', '');
482
+ }
483
+ }
484
+ }, [hasPallor, setValue, getValues]);
485
+
486
+ useEffect(() => {
487
+ if (!hasLymphadenopathy && getValues('siteLymphadenopathy')?.length) {
488
+ setValue('siteLymphadenopathy', []);
489
+ }
490
+ }, [hasLymphadenopathy, setValue, getValues]);
491
+
492
+ // Handlers
493
+ const handleNext = async () => {
494
+ const fieldsToValidate: (keyof ClinicalEncounterFormData)[] = [];
495
+
496
+ if (currentSection === 0) {
497
+ fieldsToValidate.push('symptoms');
498
+ if (hasOtherSymptom) {
499
+ fieldsToValidate.push('otherSymptomText');
500
+ }
501
+ } else if (currentSection === 1) {
502
+ if (hasSurgicalHistory) {
503
+ fieldsToValidate.push('surgeryDetails');
504
+ }
505
+ if (hasAdmissionHistory) {
506
+ fieldsToValidate.push('admissionReason');
507
+ }
508
+ if (hasOtherPastIllness) {
509
+ fieldsToValidate.push('otherPastIllnessText');
510
+ }
511
+ if (medicationsTaken === YES_ANSWER_UUID) {
512
+ fieldsToValidate.push('medicationDetails');
513
+ }
514
+ } else if (currentSection === 2) {
515
+ if (hasAbnormalGait) {
516
+ fieldsToValidate.push('gaitFinding');
517
+ }
518
+ if (hasJaundice) {
519
+ fieldsToValidate.push('jaundiceGrading');
520
+ }
521
+ if (hasCyanosis) {
522
+ fieldsToValidate.push('cyanosisType');
523
+ }
524
+ if (hasDehydration) {
525
+ fieldsToValidate.push('dehydrationType');
526
+ }
527
+ if (hasFingerClubbing) {
528
+ fieldsToValidate.push('fingerclubGrading');
529
+ }
530
+ if (hasOedema) {
531
+ fieldsToValidate.push('oedemaLocation', 'oedemaType', 'oedemaGrading');
532
+ }
533
+ if (hasPallor) {
534
+ fieldsToValidate.push('pallorLocation', 'pallorGrading');
535
+ }
536
+ if (hasLymphadenopathy) {
537
+ fieldsToValidate.push('siteLymphadenopathy');
538
+ }
539
+ }
540
+
541
+ const isValid = fieldsToValidate.length > 0 ? await trigger(fieldsToValidate) : true;
542
+
543
+ if (isValid && currentSection < sections.length - 1) {
544
+ setCurrentSection(currentSection + 1);
545
+ }
546
+ };
547
+
548
+ const handleBack = () => {
549
+ if (currentSection > 0) {
550
+ setCurrentSection(currentSection - 1);
551
+ }
552
+ };
553
+
554
+ const onSubmit = (data: ClinicalEncounterFormData) => {
555
+ // TODO: Submit data to backend
556
+ closeWorkspace();
557
+ };
558
+
559
+ const handleError = (errors: any) => {
560
+ if (errors.symptoms || errors.otherSymptomText) {
561
+ setCurrentSection(0);
562
+ } else if (
563
+ errors.surgeryDetails ||
564
+ errors.admissionReason ||
565
+ errors.otherPastIllnessText ||
566
+ errors.medicationDetails
567
+ ) {
568
+ setCurrentSection(1);
569
+ } else if (
570
+ errors.gaitFinding ||
571
+ errors.jaundiceGrading ||
572
+ errors.cyanosisType ||
573
+ errors.dehydrationType ||
574
+ errors.fingerclubGrading ||
575
+ errors.oedemaLocation ||
576
+ errors.oedemaType ||
577
+ errors.oedemaGrading ||
578
+ errors.pallorLocation ||
579
+ errors.pallorGrading ||
580
+ errors.siteLymphadenopathy
581
+ ) {
582
+ setCurrentSection(2);
583
+ }
584
+ };
585
+
586
+ const isLastSection = currentSection === sections.length - 1;
587
+ const isFirstSection = currentSection === 0;
588
+
589
+ // Render section content
590
+ const renderSectionContent = () => {
591
+ switch (currentSection) {
592
+ case 0: // Symptoms
593
+ return (
594
+ <div className={styles.sectionContent}>
595
+ <h3 className={styles.sectionTitle}>{t('symptoms', 'Symptoms')}</h3>
596
+ <Stack gap={5}>
597
+ <Layer>
598
+ <Controller
599
+ name="symptoms"
600
+ control={control}
601
+ render={({ field }) => {
602
+ const currentSymptoms = field.value || [];
603
+ return (
604
+ <div className={styles.selectedSymptomsContainer}>
605
+ {currentSymptoms.length > 0 && (
606
+ <div className={styles.selectedSymptomsTags}>
607
+ {currentSymptoms.map((item) => (
608
+ <Tag
609
+ key={item.id}
610
+ type="cyan"
611
+ filter
612
+ onClose={() => {
613
+ const updatedSymptoms = currentSymptoms.filter((s) => s.id !== item.id);
614
+ field.onChange(updatedSymptoms);
615
+ }}
616
+ className={styles.selectedSymptomTag}>
617
+ {item.text}
618
+ </Tag>
619
+ ))}
620
+ </div>
621
+ )}
622
+ <FilterableMultiSelect
623
+ id="symptoms-select"
624
+ className={styles.symptomsMultiSelect}
625
+ titleText={t('symptom', '* Symptoms')}
626
+ placeholder={t('searchSymptom', 'Search and select symptoms')}
627
+ items={symptomOptions}
628
+ itemToString={(item) => (item ? item.text : '')}
629
+ selectedItems={currentSymptoms}
630
+ onChange={({ selectedItems }: { selectedItems: Array<GenericItem> }) => {
631
+ field.onChange(selectedItems || []);
632
+ }}
633
+ invalid={!!errors.symptoms}
634
+ invalidText={errors.symptoms?.message}
635
+ />
636
+ </div>
637
+ );
638
+ }}
639
+ />
640
+ </Layer>
641
+
642
+ {symptoms && symptoms.length > 0 && (
643
+ <Layer>
644
+ <Controller
645
+ name="onsetStatus"
646
+ control={control}
647
+ render={({ field }) => (
648
+ <RadioButtonGroup
649
+ legendText={t('onsetStatus', 'Onset of symptoms')}
650
+ name="onsetStatus"
651
+ valueSelected={field.value || ''}
652
+ onChange={field.onChange}>
653
+ {onsetStatusAnswers.map((answer) => (
654
+ <RadioButton
655
+ key={answer.uuid}
656
+ labelText={answer.label}
657
+ value={answer.uuid}
658
+ id={`onset-${answer.uuid}`}
659
+ />
660
+ ))}
661
+ </RadioButtonGroup>
662
+ )}
663
+ />
664
+ </Layer>
665
+ )}
666
+
667
+ {hasCoughSymptom && !isLoadingTbAnswers && (
668
+ <Layer>
669
+ <Controller
670
+ name="tbContact"
671
+ control={control}
672
+ render={({ field }) => (
673
+ <RadioButtonGroup
674
+ legendText={t(
675
+ 'tbContactQuestion',
676
+ 'In the past 12 months, have you been in close contact with anyone who has active tuberculosis (TB)?',
677
+ )}
678
+ name="tbContact"
679
+ valueSelected={field.value || ''}
680
+ onChange={field.onChange}>
681
+ {tbContactAnswers.map((answer) => (
682
+ <RadioButton
683
+ key={answer.uuid}
684
+ labelText={answer.display}
685
+ value={answer.uuid}
686
+ id={`tb-${answer.uuid}`}
687
+ />
688
+ ))}
689
+ </RadioButtonGroup>
690
+ )}
691
+ />
692
+ </Layer>
693
+ )}
694
+
695
+ {hasOtherSymptom && (
696
+ <Controller
697
+ name="otherSymptomText"
698
+ control={control}
699
+ render={({ field }) => (
700
+ <TextArea
701
+ id="other-symptom-details"
702
+ labelText={t('specifyOtherSymptom', '* Specify other symptom(s)')}
703
+ placeholder={t('enterSymptomDetails', 'Enter details about the other symptom(s)')}
704
+ value={field.value || ''}
705
+ onChange={(e) => field.onChange(e.target.value)}
706
+ onBlur={field.onBlur}
707
+ invalid={!!errors.otherSymptomText}
708
+ invalidText={errors.otherSymptomText?.message}
709
+ rows={3}
710
+ />
711
+ )}
712
+ />
713
+ )}
714
+ </Stack>
715
+ </div>
716
+ );
717
+
718
+ case 1: // History of present illness
719
+ return (
720
+ <div className={styles.sectionContent}>
721
+ <h3 className={styles.sectionTitle}>{t('presentIllness', 'History of present illness')}</h3>
722
+ <Stack gap={5}>
723
+ <Layer>
724
+ <Controller
725
+ name="pastIllness"
726
+ control={control}
727
+ render={({ field }) => {
728
+ const currentPastIllness = field.value || [];
729
+ return (
730
+ <div className={styles.selectedSymptomsContainer}>
731
+ {currentPastIllness.length > 0 && (
732
+ <div className={styles.selectedSymptomsTags}>
733
+ {currentPastIllness.map((item) => (
734
+ <Tag
735
+ key={item.id}
736
+ type="cyan"
737
+ filter
738
+ onClose={() => {
739
+ const updatedIllness = currentPastIllness.filter((s) => s.id !== item.id);
740
+ field.onChange(updatedIllness);
741
+ }}
742
+ className={styles.selectedSymptomTag}>
743
+ {item.text}
744
+ </Tag>
745
+ ))}
746
+ </div>
747
+ )}
748
+ <FilterableMultiSelect
749
+ id="past-illness-select"
750
+ className={styles.symptomsMultiSelect}
751
+ titleText={t('pastIllness', 'Past Illness')}
752
+ placeholder={t('selectPastIllness', 'Select past illness')}
753
+ items={pastIllnessOptions}
754
+ itemToString={(item) => (item ? item.text : '')}
755
+ selectedItems={currentPastIllness}
756
+ onChange={({ selectedItems }: { selectedItems: Array<GenericItem> }) => {
757
+ field.onChange(selectedItems || []);
758
+ }}
759
+ />
760
+ </div>
761
+ );
762
+ }}
763
+ />
764
+ </Layer>
765
+
766
+ {hasSurgicalHistory && (
767
+ <Controller
768
+ name="surgeryDetails"
769
+ control={control}
770
+ render={({ field }) => (
771
+ <TextArea
772
+ id="surgery-details"
773
+ className={styles.textAreaInput}
774
+ labelText={t('surgeryPerformed', '* Surgery performed')}
775
+ placeholder={t('addSurgeryDetails', 'Add details of surgery performed')}
776
+ value={field.value || ''}
777
+ onChange={(e) => field.onChange(e.target.value)}
778
+ onBlur={field.onBlur}
779
+ invalid={!!errors.surgeryDetails}
780
+ invalidText={errors.surgeryDetails?.message}
781
+ rows={4}
782
+ />
783
+ )}
784
+ />
785
+ )}
786
+
787
+ {hasAdmissionHistory && (
788
+ <Controller
789
+ name="admissionReason"
790
+ control={control}
791
+ render={({ field }) => (
792
+ <TextArea
793
+ id="admission-reason"
794
+ className={styles.textAreaInput}
795
+ labelText={t('reasonForAdmission', '* Reason for admission')}
796
+ placeholder={t('addReasonForAdmission', 'Add reason for admission')}
797
+ value={field.value || ''}
798
+ onChange={(e) => field.onChange(e.target.value)}
799
+ onBlur={field.onBlur}
800
+ invalid={!!errors.admissionReason}
801
+ invalidText={errors.admissionReason?.message}
802
+ rows={4}
803
+ />
804
+ )}
805
+ />
806
+ )}
807
+
808
+ {hasOtherPastIllness && (
809
+ <Controller
810
+ name="otherPastIllnessText"
811
+ control={control}
812
+ render={({ field }) => (
813
+ <TextArea
814
+ id="other-past-illness"
815
+ className={styles.textAreaInput}
816
+ labelText={t('specifyOtherIllness', '* Specify other illness')}
817
+ placeholder={t('enterOtherIllnessDetails', 'Enter details about the other illness')}
818
+ value={field.value || ''}
819
+ onChange={(e) => field.onChange(e.target.value)}
820
+ onBlur={field.onBlur}
821
+ invalid={!!errors.otherPastIllnessText}
822
+ invalidText={errors.otherPastIllnessText?.message}
823
+ rows={4}
824
+ />
825
+ )}
826
+ />
827
+ )}
828
+
829
+ <Layer>
830
+ <Controller
831
+ name="medicationsTaken"
832
+ control={control}
833
+ render={({ field }) => (
834
+ <RadioButtonGroup
835
+ legendText={t(
836
+ 'medicationsTakenQuestion',
837
+ 'Have you taken any prescription or over-the-counter medications, or herbal remedies before this visit?',
838
+ )}
839
+ name="medicationsTaken"
840
+ className={styles.radioButtonGroup}
841
+ valueSelected={field.value || ''}
842
+ onChange={field.onChange}>
843
+ {tbContactAnswers.map((answer) => (
844
+ <RadioButton
845
+ key={answer.uuid}
846
+ labelText={answer.display}
847
+ value={answer.uuid}
848
+ id={`medications-${answer.uuid}`}
849
+ />
850
+ ))}
851
+ </RadioButtonGroup>
852
+ )}
853
+ />
854
+ </Layer>
855
+
856
+ {medicationsTaken === YES_ANSWER_UUID && (
857
+ <Controller
858
+ name="medicationDetails"
859
+ control={control}
860
+ render={({ field }) => (
861
+ <TextArea
862
+ id="medications-details"
863
+ className={styles.textAreaInput}
864
+ labelText={t('medicationsTaken', '* Medications taken')}
865
+ placeholder={t('addMedicationsTaken', 'Add medications taken')}
866
+ value={field.value || ''}
867
+ onChange={(e) => field.onChange(e.target.value)}
868
+ onBlur={field.onBlur}
869
+ invalid={!!errors.medicationDetails}
870
+ invalidText={errors.medicationDetails?.message}
871
+ rows={3}
872
+ />
873
+ )}
874
+ />
875
+ )}
876
+ </Stack>
877
+ </div>
878
+ );
879
+
880
+ case 2: // Physical Examination
881
+ return (
882
+ <div className={styles.sectionContent}>
883
+ <h3 className={styles.sectionTitle}>{t('physicalExamination', 'Physical Examination')}</h3>
884
+ <Stack gap={5}>
885
+ <Layer>
886
+ <Controller
887
+ name="generalExamination"
888
+ control={control}
889
+ render={({ field }) => {
890
+ const currentExamination = field.value || [];
891
+ return (
892
+ <div className={styles.selectedSymptomsContainer}>
893
+ {currentExamination.length > 0 && (
894
+ <div className={styles.selectedSymptomsTags}>
895
+ {currentExamination.map((item) => (
896
+ <Tag
897
+ key={item.id}
898
+ type="cyan"
899
+ filter
900
+ onClose={() => {
901
+ const updatedExamination = currentExamination.filter((e) => e.id !== item.id);
902
+ field.onChange(updatedExamination);
903
+ }}
904
+ className={styles.selectedSymptomTag}>
905
+ {item.text}
906
+ </Tag>
907
+ ))}
908
+ </div>
909
+ )}
910
+ <FilterableMultiSelect
911
+ id="general-examination-select"
912
+ className={styles.symptomsMultiSelect}
913
+ titleText={t('generalExamination', 'General Examination')}
914
+ placeholder={t('selectGeneralExamination', 'Select general examination findings')}
915
+ items={generalExaminationOptions}
916
+ itemToString={(item) => (item ? item.text : '')}
917
+ selectedItems={currentExamination}
918
+ onChange={({ selectedItems }: { selectedItems: Array<GenericItem> }) => {
919
+ field.onChange(selectedItems || []);
920
+ }}
921
+ />
922
+ </div>
923
+ );
924
+ }}
925
+ />
926
+ </Layer>
927
+
928
+ {/* Abnormal Gait */}
929
+ {hasAbnormalGait && (
930
+ <Layer>
931
+ <Controller
932
+ name="gaitFinding"
933
+ control={control}
934
+ render={({ field }) => (
935
+ <RadioButtonGroup
936
+ legendText={t('gaitFinding', '* Gait Finding')}
937
+ name="gaitFinding"
938
+ orientation="vertical"
939
+ valueSelected={field.value || ''}
940
+ onChange={field.onChange}>
941
+ {gaitFindingOptions.map((option) => (
942
+ <RadioButton
943
+ key={option.uuid}
944
+ className={styles.radioButtonOption}
945
+ labelText={option.label}
946
+ value={option.uuid}
947
+ id={`gait-${option.uuid}`}
948
+ />
949
+ ))}
950
+ </RadioButtonGroup>
951
+ )}
952
+ />
953
+ {errors.gaitFinding && <div className={styles.errorText}>{errors.gaitFinding.message}</div>}
954
+ </Layer>
955
+ )}
956
+
957
+ {/* Jaundice */}
958
+ {hasJaundice && (
959
+ <Layer>
960
+ <Controller
961
+ name="jaundiceGrading"
962
+ control={control}
963
+ render={({ field }) => (
964
+ <RadioButtonGroup
965
+ legendText={t('jaundiceGrading', '* Jaundice Grading')}
966
+ name="jaundiceGrading"
967
+ orientation="vertical"
968
+ valueSelected={field.value || ''}
969
+ onChange={field.onChange}>
970
+ {jaundiceGradingOptions.map((option) => (
971
+ <RadioButton
972
+ key={option.uuid}
973
+ className={styles.radioButtonOption}
974
+ labelText={option.label}
975
+ value={option.uuid}
976
+ id={`jaundice-${option.uuid}`}
977
+ />
978
+ ))}
979
+ </RadioButtonGroup>
980
+ )}
981
+ />
982
+ {errors.jaundiceGrading && <div className={styles.errorText}>{errors.jaundiceGrading.message}</div>}
983
+ </Layer>
984
+ )}
985
+
986
+ {/* Cyanosis */}
987
+ {hasCyanosis && (
988
+ <Layer>
989
+ <Controller
990
+ name="cyanosisType"
991
+ control={control}
992
+ render={({ field }) => (
993
+ <RadioButtonGroup
994
+ legendText={t('cyanosisType', '* Cyanosis Type')}
995
+ name="cyanosisType"
996
+ orientation="vertical"
997
+ valueSelected={field.value || ''}
998
+ onChange={field.onChange}>
999
+ {cyanosisTypeOptions.map((option) => (
1000
+ <RadioButton
1001
+ key={option.uuid}
1002
+ labelText={option.label}
1003
+ className={styles.radioButtonOption}
1004
+ value={option.uuid}
1005
+ id={`cyanosis-${option.uuid}`}
1006
+ />
1007
+ ))}
1008
+ </RadioButtonGroup>
1009
+ )}
1010
+ />
1011
+ {errors.cyanosisType && <div className={styles.errorText}>{errors.cyanosisType.message}</div>}
1012
+ </Layer>
1013
+ )}
1014
+
1015
+ {/* Dehydration */}
1016
+ {hasDehydration && (
1017
+ <Layer>
1018
+ <Controller
1019
+ name="dehydrationType"
1020
+ control={control}
1021
+ render={({ field }) => {
1022
+ const currentDehydration = field.value || [];
1023
+ return (
1024
+ <div className={styles.selectedSymptomsContainer}>
1025
+ {currentDehydration.length > 0 && (
1026
+ <div className={styles.selectedSymptomsTags}>
1027
+ {currentDehydration.map((item) => (
1028
+ <Tag
1029
+ key={item.id}
1030
+ type="cyan"
1031
+ filter
1032
+ onClose={() => {
1033
+ const updated = currentDehydration.filter((d) => d.id !== item.id);
1034
+ field.onChange(updated);
1035
+ }}
1036
+ className={styles.selectedSymptomTag}>
1037
+ {item.text}
1038
+ </Tag>
1039
+ ))}
1040
+ </div>
1041
+ )}
1042
+ <FilterableMultiSelect
1043
+ id="dehydration-type-select"
1044
+ className={styles.symptomsMultiSelect}
1045
+ titleText={t('dehydrationType', '* Dehydration Type')}
1046
+ placeholder={t('selectDehydrationType', 'Select dehydration type')}
1047
+ items={dehydrationTypeOptions}
1048
+ itemToString={(item) => (item ? item.text : '')}
1049
+ selectedItems={currentDehydration}
1050
+ onChange={({ selectedItems }: { selectedItems: Array<GenericItem> }) => {
1051
+ field.onChange(selectedItems || []);
1052
+ }}
1053
+ invalid={!!errors.dehydrationType}
1054
+ invalidText={errors.dehydrationType?.message}
1055
+ />
1056
+ </div>
1057
+ );
1058
+ }}
1059
+ />
1060
+ </Layer>
1061
+ )}
1062
+
1063
+ {/* Finger Clubbing */}
1064
+ {hasFingerClubbing && (
1065
+ <Layer>
1066
+ <Controller
1067
+ name="fingerclubGrading"
1068
+ control={control}
1069
+ render={({ field }) => (
1070
+ <RadioButtonGroup
1071
+ legendText={t('fingerclubGrading', '* Finger Clubbing Grading')}
1072
+ name="fingerclubGrading"
1073
+ valueSelected={field.value || ''}
1074
+ orientation="vertical"
1075
+ className={styles.radioButtonOption}
1076
+ onChange={field.onChange}>
1077
+ {fingerclubGradingOptions.map((option) => (
1078
+ <RadioButton
1079
+ key={option.uuid}
1080
+ labelText={option.label}
1081
+ value={option.uuid}
1082
+ id={`fingerclub-${option.uuid}`}
1083
+ />
1084
+ ))}
1085
+ </RadioButtonGroup>
1086
+ )}
1087
+ />
1088
+ {errors.fingerclubGrading && (
1089
+ <div className={styles.errorText}>{errors.fingerclubGrading.message}</div>
1090
+ )}
1091
+ </Layer>
1092
+ )}
1093
+
1094
+ {/* Oedema */}
1095
+ {hasOedema && (
1096
+ <>
1097
+ <Layer>
1098
+ <Controller
1099
+ name="oedemaLocation"
1100
+ control={control}
1101
+ render={({ field }) => {
1102
+ const currentLocation = field.value || [];
1103
+ return (
1104
+ <div className={styles.selectedSymptomsContainer}>
1105
+ {currentLocation.length > 0 && (
1106
+ <div className={styles.selectedSymptomsTags}>
1107
+ {currentLocation.map((item) => (
1108
+ <Tag
1109
+ key={item.id}
1110
+ type="cyan"
1111
+ filter
1112
+ onClose={() => {
1113
+ const updated = currentLocation.filter((l) => l.id !== item.id);
1114
+ field.onChange(updated);
1115
+ }}
1116
+ className={styles.selectedSymptomTag}>
1117
+ {item.text}
1118
+ </Tag>
1119
+ ))}
1120
+ </div>
1121
+ )}
1122
+ <FilterableMultiSelect
1123
+ id="oedema-location-select"
1124
+ className={styles.symptomsMultiSelect}
1125
+ titleText={t('oedemaLocation', '* Oedema Location')}
1126
+ placeholder={t('selectOedemaLocation', 'Select oedema location')}
1127
+ items={oedemaLocationOptions}
1128
+ itemToString={(item) => (item ? item.text : '')}
1129
+ selectedItems={currentLocation}
1130
+ onChange={({ selectedItems }: { selectedItems: Array<GenericItem> }) => {
1131
+ field.onChange(selectedItems || []);
1132
+ }}
1133
+ invalid={!!errors.oedemaLocation}
1134
+ invalidText={errors.oedemaLocation?.message}
1135
+ />
1136
+ </div>
1137
+ );
1138
+ }}
1139
+ />
1140
+ </Layer>
1141
+
1142
+ <Layer>
1143
+ <Controller
1144
+ name="oedemaType"
1145
+ control={control}
1146
+ render={({ field }) => (
1147
+ <RadioButtonGroup
1148
+ legendText={t('oedemaType', '* Oedema Type')}
1149
+ name="oedemaType"
1150
+ valueSelected={field.value || ''}
1151
+ onChange={field.onChange}>
1152
+ orientation="vertical"
1153
+ {oedemaTypeOptions.map((option) => (
1154
+ <RadioButton
1155
+ key={option.uuid}
1156
+ labelText={option.label}
1157
+ value={option.uuid}
1158
+ id={`oedema-type-${option.uuid}`}
1159
+ />
1160
+ ))}
1161
+ </RadioButtonGroup>
1162
+ )}
1163
+ />
1164
+ {errors.oedemaType && <div className={styles.errorText}>{errors.oedemaType.message}</div>}
1165
+ </Layer>
1166
+
1167
+ <Layer>
1168
+ <Controller
1169
+ name="oedemaGrading"
1170
+ control={control}
1171
+ render={({ field }) => (
1172
+ <RadioButtonGroup
1173
+ legendText={t('oedemaGrading', '* Oedema Grading')}
1174
+ name="oedemaGrading"
1175
+ orientation="vertical"
1176
+ valueSelected={field.value || ''}
1177
+ onChange={field.onChange}>
1178
+ {oedemaGradingOptions.map((option) => (
1179
+ <RadioButton
1180
+ key={option.uuid}
1181
+ labelText={option.label}
1182
+ value={option.uuid}
1183
+ id={`oedema-grading-${option.uuid}`}
1184
+ className={styles.radioButtonOption}
1185
+ />
1186
+ ))}
1187
+ </RadioButtonGroup>
1188
+ )}
1189
+ />
1190
+ {errors.oedemaGrading && <div className={styles.errorText}>{errors.oedemaGrading.message}</div>}
1191
+ </Layer>
1192
+ </>
1193
+ )}
1194
+
1195
+ {/* Pallor */}
1196
+ {hasPallor && (
1197
+ <>
1198
+ <Layer>
1199
+ <Controller
1200
+ name="pallorLocation"
1201
+ control={control}
1202
+ render={({ field }) => {
1203
+ const currentLocation = field.value || [];
1204
+ return (
1205
+ <div className={styles.selectedSymptomsContainer}>
1206
+ {currentLocation.length > 0 && (
1207
+ <div className={styles.selectedSymptomsTags}>
1208
+ {currentLocation.map((item) => (
1209
+ <Tag
1210
+ key={item.id}
1211
+ type="cyan"
1212
+ filter
1213
+ onClose={() => {
1214
+ const updated = currentLocation.filter((l) => l.id !== item.id);
1215
+ field.onChange(updated);
1216
+ }}
1217
+ className={styles.selectedSymptomTag}>
1218
+ {item.text}
1219
+ </Tag>
1220
+ ))}
1221
+ </div>
1222
+ )}
1223
+ <FilterableMultiSelect
1224
+ id="pallor-location-select"
1225
+ className={styles.symptomsMultiSelect}
1226
+ titleText={t('pallorLocation', '* Pallor Location')}
1227
+ placeholder={t('selectPallorLocation', 'Select pallor location')}
1228
+ items={pallorLocationOptions}
1229
+ itemToString={(item) => (item ? item.text : '')}
1230
+ selectedItems={currentLocation}
1231
+ onChange={({ selectedItems }: { selectedItems: Array<GenericItem> }) => {
1232
+ field.onChange(selectedItems || []);
1233
+ }}
1234
+ invalid={!!errors.pallorLocation}
1235
+ invalidText={errors.pallorLocation?.message}
1236
+ />
1237
+ </div>
1238
+ );
1239
+ }}
1240
+ />
1241
+ </Layer>
1242
+
1243
+ <Layer>
1244
+ <Controller
1245
+ name="pallorGrading"
1246
+ control={control}
1247
+ render={({ field }) => (
1248
+ <RadioButtonGroup
1249
+ legendText={t('pallorGrading', '* Pallor Grading')}
1250
+ name="pallorGrading"
1251
+ orientation="vertical"
1252
+ valueSelected={field.value || ''}
1253
+ onChange={field.onChange}>
1254
+ {pallorGradingOptions.map((option) => (
1255
+ <RadioButton
1256
+ key={option.uuid}
1257
+ labelText={option.label}
1258
+ value={option.uuid}
1259
+ className={styles.radioButtonOption}
1260
+ id={`pallor-grading-${option.uuid}`}
1261
+ />
1262
+ ))}
1263
+ </RadioButtonGroup>
1264
+ )}
1265
+ />
1266
+ {errors.pallorGrading && <div className={styles.errorText}>{errors.pallorGrading.message}</div>}
1267
+ </Layer>
1268
+ </>
1269
+ )}
1270
+
1271
+ {/* Lymphadenopathy */}
1272
+ {hasLymphadenopathy && (
1273
+ <Layer>
1274
+ <Controller
1275
+ name="siteLymphadenopathy"
1276
+ control={control}
1277
+ render={({ field }) => {
1278
+ const currentSite = field.value || [];
1279
+ return (
1280
+ <div className={styles.selectedSymptomsContainer}>
1281
+ {currentSite.length > 0 && (
1282
+ <div className={styles.selectedSymptomsTags}>
1283
+ {currentSite.map((item) => (
1284
+ <Tag
1285
+ key={item.id}
1286
+ type="cyan"
1287
+ filter
1288
+ onClose={() => {
1289
+ const updated = currentSite.filter((s) => s.id !== item.id);
1290
+ field.onChange(updated);
1291
+ }}
1292
+ className={styles.selectedSymptomTag}>
1293
+ {item.text}
1294
+ </Tag>
1295
+ ))}
1296
+ </div>
1297
+ )}
1298
+ <FilterableMultiSelect
1299
+ id="lymphadenopathy-site-select"
1300
+ className={styles.symptomsMultiSelect}
1301
+ titleText={t('siteLymphadenopathy', '* Lymphadenopathy Site')}
1302
+ placeholder={t('selectLymphadenopathySite', 'Select lymphadenopathy site')}
1303
+ items={siteLymphadenopathyOptions}
1304
+ itemToString={(item) => (item ? item.text : '')}
1305
+ selectedItems={currentSite}
1306
+ onChange={({ selectedItems }: { selectedItems: Array<GenericItem> }) => {
1307
+ field.onChange(selectedItems || []);
1308
+ }}
1309
+ invalid={!!errors.siteLymphadenopathy}
1310
+ invalidText={errors.siteLymphadenopathy?.message}
1311
+ />
1312
+ </div>
1313
+ );
1314
+ }}
1315
+ />
1316
+ </Layer>
1317
+ )}
1318
+ <Controller
1319
+ name="generalExaminationNotes"
1320
+ control={control}
1321
+ render={({ field }) => (
1322
+ <TextArea
1323
+ id="general-examination-notes"
1324
+ labelText={t('generalExaminationNotes', 'Additional Notes')}
1325
+ placeholder={t(
1326
+ 'enterGeneralExaminationNotes',
1327
+ 'Enter any additional notes or observations from the physical examination',
1328
+ )}
1329
+ value={field.value || ''}
1330
+ onChange={(e) => field.onChange(e.target.value)}
1331
+ onBlur={field.onBlur}
1332
+ rows={4}
1333
+ />
1334
+ )}
1335
+ />
1336
+ </Stack>
1337
+ </div>
1338
+ );
1339
+ case 3: // Diagnosis
1340
+ return (
1341
+ <div className={styles.sectionContent}>
1342
+ <h3 className={styles.sectionTitle}>{t('diagnosis', 'Diagnosis')}</h3>
1343
+ <Stack gap={5}>
1344
+ {/* Primary Diagnosis */}
1345
+ <Controller
1346
+ name="primaryDiagnosis"
1347
+ control={control}
1348
+ render={({ field }) => (
1349
+ <DiagnosisAutosuggest
1350
+ id="primary-diagnosis"
1351
+ labelText={t('primaryDiagnosis', 'Primary Diagnosis')}
1352
+ placeholder={t('searchPrimaryDiagnosis', 'Search and select primary diagnosis')}
1353
+ selectedDiagnoses={(field.value || []) as Array<GenericItem>}
1354
+ onChange={field.onChange}
1355
+ invalid={!!errors.primaryDiagnosis}
1356
+ invalidText={errors.primaryDiagnosis?.message}
1357
+ />
1358
+ )}
1359
+ />
1360
+
1361
+ {/* Secondary Diagnosis */}
1362
+ <Controller
1363
+ name="secondaryDiagnosis"
1364
+ control={control}
1365
+ render={({ field }) => (
1366
+ <DiagnosisAutosuggest
1367
+ id="secondary-diagnosis"
1368
+ labelText={t('secondaryDiagnosis', 'Secondary Diagnosis (Optional)')}
1369
+ placeholder={t('searchSecondaryDiagnosis', 'Search and select secondary diagnosis')}
1370
+ selectedDiagnoses={(field.value || []) as Array<GenericItem>}
1371
+ onChange={field.onChange}
1372
+ invalid={!!errors.secondaryDiagnosis}
1373
+ invalidText={errors.secondaryDiagnosis?.message}
1374
+ />
1375
+ )}
1376
+ />
1377
+
1378
+ <Controller
1379
+ name="diagnosisNotes"
1380
+ control={control}
1381
+ render={({ field }) => (
1382
+ <TextArea
1383
+ id="diagnosis-notes"
1384
+ labelText={t('anyAdditionalNotes', 'Any Additional Notes')}
1385
+ placeholder={t('enterAnyAdditionalNotes', 'Enter any additional notes')}
1386
+ value={field.value || ''}
1387
+ onChange={(e) => field.onChange(e.target.value)}
1388
+ onBlur={field.onBlur}
1389
+ rows={4}
1390
+ />
1391
+ )}
1392
+ />
1393
+ </Stack>
1394
+ </div>
1395
+ );
1396
+
1397
+ case 4: // Procedures
1398
+ return (
1399
+ <div className={styles.sectionContent}>
1400
+ <h3 className={styles.sectionTitle}>{t('procedures', 'Procedures')}</h3>
1401
+ <BillableServicesSelector
1402
+ control={control}
1403
+ fieldName="procedures"
1404
+ key="procedures-selector" // 👈 ADD THIS
1405
+ lineItems={procedureOptionsWithPrices}
1406
+ isLoading={procedureServices.isLoading}
1407
+ paymentInfo={paymentInfo}
1408
+ title={t('searchProcedures', 'Search procedures')}
1409
+ searchPlaceholder={t('searchAndSelectProcedures', 'Search and select procedures')}
1410
+ errors={errors.procedures}
1411
+ />
1412
+ </div>
1413
+ );
1414
+
1415
+ case 5: // Laboratory
1416
+ return (
1417
+ <div className={styles.sectionContent}>
1418
+ <h3 className={styles.sectionTitle}>{t('laboratory', 'Laboratory Tests')}</h3>
1419
+ <BillableServicesSelector
1420
+ key="lab-selector" // 👈 ADD THIS
1421
+ control={control}
1422
+ fieldName="labTests"
1423
+ lineItems={labOptionsWithPrices}
1424
+ isLoading={labServices.isLoading}
1425
+ paymentInfo={paymentInfo}
1426
+ title={t('searchLabTests', 'Search laboratory tests')}
1427
+ searchPlaceholder={t('searchAndSelectLabTests', 'Search and select laboratory tests')}
1428
+ errors={errors.labTests}
1429
+ />
1430
+ </div>
1431
+ );
1432
+
1433
+ case 6: // Imaging
1434
+ return (
1435
+ <div className={styles.sectionContent}>
1436
+ <h3 className={styles.sectionTitle}>{t('imaging', 'Imaging Studies')}</h3>
1437
+ <BillableServicesSelector
1438
+ control={control}
1439
+ key="imaging-selector" // 👈 ADD THIS
1440
+ fieldName="imagingTests"
1441
+ lineItems={imagingOptionsWithPrices}
1442
+ isLoading={imagingServices.isLoading}
1443
+ paymentInfo={paymentInfo}
1444
+ title={t('searchImagingTests', 'Search imaging studies')}
1445
+ searchPlaceholder={t('searchAndSelectImagingTests', 'Search and select imaging studies')}
1446
+ errors={errors.imagingTests}
1447
+ />
1448
+ </div>
1449
+ );
1450
+ case 7: // Notes/Instructions
1451
+ return (
1452
+ <div className={styles.sectionContent}>
1453
+ <h3 className={styles.sectionTitle}>{t('notesInstructions', 'Notes/Instructions')}</h3>
1454
+ <Controller
1455
+ name="notes"
1456
+ control={control}
1457
+ render={({ field }) => (
1458
+ <TextArea
1459
+ id="notes-instructions"
1460
+ labelText={t('notesInstructions', 'Notes/Instructions')}
1461
+ placeholder={t(
1462
+ 'enterNotesInstructions',
1463
+ 'Enter any additional notes or instructions for the patient',
1464
+ )}
1465
+ value={field.value || ''}
1466
+ onChange={(e) => field.onChange(e.target.value)}
1467
+ onBlur={field.onBlur}
1468
+ rows={6}
1469
+ />
1470
+ )}
1471
+ />
1472
+ </div>
1473
+ );
1474
+
1475
+ case 8:
1476
+ return (
1477
+ <div className={styles.sectionContent}>
1478
+ <h3 className={styles.sectionTitle}>{t('outcome', 'Outcome')}</h3>
1479
+ <Layer>
1480
+ <Controller
1481
+ name="patientOutcome"
1482
+ control={control}
1483
+ render={({ field }) => (
1484
+ <RadioButtonGroup
1485
+ legendText={t('patientOutcome', '* Patient Outcome')}
1486
+ name="patientOutcome"
1487
+ orientation="vertical"
1488
+ valueSelected={field.value || ''}
1489
+ onChange={field.onChange}>
1490
+ {patientOutcomeOptions?.map((option) => (
1491
+ <RadioButton
1492
+ key={option.uuid}
1493
+ labelText={option.label}
1494
+ value={option.uuid}
1495
+ className={styles.radioButtonOption}
1496
+ id={`pallor-grading-${option.uuid}`}
1497
+ />
1498
+ ))}
1499
+ </RadioButtonGroup>
1500
+ )}
1501
+ />
1502
+ {errors.pallorGrading && <div className={styles.errorText}>{errors.pallorGrading.message}</div>}
1503
+ </Layer>
1504
+ </div>
1505
+ );
1506
+
1507
+ default:
1508
+ return null;
1509
+ }
1510
+ };
1511
+
1512
+ return (
1513
+ <div className={styles.workspaceWrapper}>
1514
+ <div className={styles.patientHeader}>
1515
+ <span className={styles.patientName}>{capitalize(person?.display)}</span>
1516
+ <span className={styles.patientDetails}>· {openmrsId} </span>
1517
+ <span className={styles.patientDetails}>
1518
+ · {person?.gender} · {person?.age} {t('years', 'yrs')}
1519
+ </span>
1520
+ </div>
1521
+ <div className={styles.workspaceContainer}>
1522
+ <div className={styles.sidebar}>
1523
+ <TreeView
1524
+ key={currentSection}
1525
+ label={t('clinicalEncounter', 'Clinical Encounter')}
1526
+ selected={[currentSection.toString()]}
1527
+ active={currentSection.toString()}
1528
+ hideLabel
1529
+ className={styles.treeView}>
1530
+ {sections.map((section) => (
1531
+ <TreeNode
1532
+ key={section.id}
1533
+ id={section.id.toString()}
1534
+ label={section.label}
1535
+ onClick={() => setCurrentSection(section.id)}
1536
+ isSelected={currentSection === section.id}
1537
+ />
1538
+ ))}
1539
+ </TreeView>
1540
+ </div>
1541
+ <div className={styles.mainContent}>
1542
+ <Form
1543
+ className={styles.formWrapper}
1544
+ data-openmrs-role="clinical-encounter-form"
1545
+ onSubmit={handleFormSubmit(onSubmit, handleError)}>
1546
+ <div className={styles.formContent}>{renderSectionContent()}</div>
1547
+
1548
+ <div className={styles.navigationButtons}>
1549
+ {isLastSection ? (
1550
+ <ButtonSet className={styles.buttonSet}>
1551
+ <Button kind="secondary" className={styles.button} size="lg" onClick={() => closeWorkspace()}>
1552
+ {t('discard', 'Discard')}
1553
+ </Button>
1554
+ <Button className={styles.button} kind="primary" size="lg" type="submit" disabled={isSubmitting}>
1555
+ {t('saveAndClose', 'Save and close')}
1556
+ </Button>
1557
+ </ButtonSet>
1558
+ ) : (
1559
+ <ButtonSet className={styles.buttonSet}>
1560
+ <Button
1561
+ className={styles.button}
1562
+ kind="secondary"
1563
+ size="lg"
1564
+ onClick={handleBack}
1565
+ disabled={isFirstSection}>
1566
+ {t('back', 'Back')}
1567
+ </Button>
1568
+ <Button className={styles.button} kind="primary" size="lg" onClick={handleNext}>
1569
+ {t('next', 'Next')}
1570
+ </Button>
1571
+ </ButtonSet>
1572
+ )}
1573
+ </div>
1574
+ </Form>
1575
+ </div>
1576
+ </div>
1577
+ </div>
1578
+ );
1579
+ };
1580
+
1581
+ export default ClinicalEncounterWorkspace;