@kenyaemr/esm-patient-clinical-view-app 5.4.2-pre.2592 → 5.4.2-pre.2598

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 (39) hide show
  1. package/.turbo/turbo-build.log +6 -6
  2. package/dist/127.js +1 -1
  3. package/dist/152.js +3 -3
  4. package/dist/152.js.map +1 -1
  5. package/dist/481.js +66 -0
  6. package/dist/481.js.map +1 -0
  7. package/dist/671.js +1 -1
  8. package/dist/671.js.map +1 -1
  9. package/dist/941.js +1 -0
  10. package/dist/941.js.map +1 -0
  11. package/dist/kenyaemr-esm-patient-clinical-view-app.js +2 -2
  12. package/dist/kenyaemr-esm-patient-clinical-view-app.js.buildmanifest.json +55 -55
  13. package/dist/kenyaemr-esm-patient-clinical-view-app.js.map +1 -1
  14. package/dist/main.js +87 -14
  15. package/dist/main.js.map +1 -1
  16. package/dist/routes.json +1 -1
  17. package/package.json +1 -1
  18. package/src/config-schema.ts +144 -0
  19. package/src/index.ts +2 -2
  20. package/src/maternal-and-child-health/partography/labour-delivery.scss +6 -7
  21. package/src/maternal-and-child-health/partography/partograph.component.tsx +487 -151
  22. package/src/maternal-and-child-health/partography/partography-data-form.component.tsx +434 -0
  23. package/src/maternal-and-child-health/partography/partography-data-form.scss +50 -0
  24. package/src/maternal-and-child-health/partography/partography-link.component.tsx +21 -0
  25. package/src/maternal-and-child-health/partography/partography.resource.ts +1024 -0
  26. package/src/maternal-and-child-health/partography/partography.scss +378 -0
  27. package/src/maternal-and-child-health/partography/types/index.ts +980 -0
  28. package/translations/en.json +11 -1
  29. package/dist/287.js +0 -1
  30. package/dist/287.js.map +0 -1
  31. package/dist/98.js +0 -1
  32. package/dist/98.js.map +0 -1
  33. package/src/maternal-and-child-health/partography/cervical-dilation.component.tsx +0 -16
  34. package/src/maternal-and-child-health/partography/contraction-level.component.tsx +0 -16
  35. package/src/maternal-and-child-health/partography/descent-of-head.component.tsx +0 -16
  36. package/src/maternal-and-child-health/partography/foetal-heart-rate.component.tsx +0 -17
  37. package/src/maternal-and-child-health/partography/membrane-amniotic-fluid-moulding.component.tsx +0 -17
  38. package/src/maternal-and-child-health/partography/partograph-chart.scss +0 -94
  39. package/src/maternal-and-child-health/partography/partograph-chart.tsx +0 -176
@@ -0,0 +1,434 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { useForm, Controller } from 'react-hook-form';
4
+ import { Modal, TextArea, TextInput, NumberInput, Select, SelectItem, Form, Grid, Column } from '@carbon/react';
5
+ import {
6
+ AMNIOTIC_FLUID_OPTIONS,
7
+ MOULDING_OPTIONS,
8
+ DESCENT_OF_HEAD_OPTIONS,
9
+ CONTRACTION_INTENSITY_OPTIONS,
10
+ URINE_LEVEL_OPTIONS,
11
+ BLOOD_GROUP_OPTIONS,
12
+ ADMISSION_OPTIONS,
13
+ TIME_SLOT_OPTIONS,
14
+ EVENT_TYPE_OPTIONS,
15
+ INPUT_RANGES,
16
+ getMeasurementLabel,
17
+ getFieldLabel,
18
+ getFieldPlaceholder,
19
+ getInputRangePlaceholder,
20
+ getGraphDataProcessor,
21
+ } from './types';
22
+ import styles from './partography-data-form.scss';
23
+
24
+ type PartographyFormData = {
25
+ admission: string;
26
+ bg: string;
27
+ am: string;
28
+ measurementValue: string;
29
+ systolic: string;
30
+ diastolic: string;
31
+ proteinLevel: string;
32
+ glucoseLevel: string;
33
+ ketoneLevel: string;
34
+ medication: string;
35
+ dosage: string;
36
+ eventType: string;
37
+ eventDescription: string;
38
+ amnioticFluid: string;
39
+ moulding: string;
40
+ };
41
+
42
+ type PartographyDataFormProps = {
43
+ isOpen: boolean;
44
+ onClose: () => void;
45
+ onSubmit: (data: any) => void;
46
+ graphType: string;
47
+ graphTitle: string;
48
+ patient?: fhir.Patient;
49
+ };
50
+
51
+ const PartographyDataForm: React.FC<PartographyDataFormProps> = ({
52
+ isOpen,
53
+ onClose,
54
+ onSubmit,
55
+ graphType,
56
+ graphTitle,
57
+ patient,
58
+ }) => {
59
+ const { t } = useTranslation();
60
+
61
+ const getPatientInfo = (patient?: fhir.Patient) => {
62
+ const getName = (): string => {
63
+ if (!patient?.name?.[0]) {
64
+ return '';
65
+ }
66
+ const name = patient.name[0];
67
+ const given = name.given?.join(' ') || '';
68
+ const family = name.family || '';
69
+ return `${given} ${family}`.trim();
70
+ };
71
+
72
+ const getGender = (): string => (patient?.gender ? t(patient.gender, patient.gender) : '');
73
+
74
+ const getAge = (): string => {
75
+ if (!patient?.birthDate) {
76
+ return '';
77
+ }
78
+ const birthDate = new Date(patient.birthDate);
79
+ const today = new Date();
80
+ const age = today.getFullYear() - birthDate.getFullYear();
81
+ const monthDiff = today.getMonth() - birthDate.getMonth();
82
+ const adjustedAge = monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate()) ? age - 1 : age;
83
+ return `${adjustedAge} ${t('yearsOld', 'years old')}`;
84
+ };
85
+
86
+ return { name: getName(), gender: getGender(), age: getAge() };
87
+ };
88
+
89
+ const patientInfo = getPatientInfo(patient);
90
+
91
+ const { control, handleSubmit, reset } = useForm<PartographyFormData>({
92
+ defaultValues: {
93
+ admission: '',
94
+ bg: '',
95
+ am: '',
96
+ measurementValue: '',
97
+ systolic: '',
98
+ diastolic: '',
99
+ proteinLevel: '',
100
+ glucoseLevel: '',
101
+ ketoneLevel: '',
102
+ medication: '',
103
+ dosage: '',
104
+ eventType: '',
105
+ eventDescription: '',
106
+ amnioticFluid: '',
107
+ moulding: '',
108
+ },
109
+ });
110
+
111
+ const createNumberInput = (
112
+ id: string,
113
+ name: keyof PartographyFormData,
114
+ label: string,
115
+ placeholder: string,
116
+ range: any,
117
+ required = true,
118
+ ) => (
119
+ <Controller
120
+ name={name}
121
+ control={control}
122
+ rules={required ? { required: t('fieldRequired', 'This field is required') } : {}}
123
+ render={({ field, fieldState }) => (
124
+ <NumberInput
125
+ id={id}
126
+ label={label}
127
+ placeholder={placeholder}
128
+ value={field.value || ''}
129
+ onChange={(e, { value }) => field.onChange(String(value))}
130
+ min={range.min}
131
+ max={range.max}
132
+ step={range.step}
133
+ invalid={!!fieldState.error}
134
+ invalidText={fieldState.error?.message}
135
+ required={required}
136
+ />
137
+ )}
138
+ />
139
+ );
140
+
141
+ const createSelect = (
142
+ id: string,
143
+ name: keyof PartographyFormData,
144
+ label: string,
145
+ options: readonly any[],
146
+ required = false,
147
+ ) => (
148
+ <Controller
149
+ name={name}
150
+ control={control}
151
+ rules={required ? { required: t('fieldRequired', 'This field is required') } : {}}
152
+ render={({ field, fieldState }) => (
153
+ <Select
154
+ id={id}
155
+ labelText={label}
156
+ value={field.value}
157
+ onChange={(e) => field.onChange((e.target as HTMLSelectElement).value)}
158
+ invalid={!!fieldState.error}
159
+ invalidText={fieldState.error?.message}
160
+ required={required}>
161
+ {options.map((option) => (
162
+ <SelectItem
163
+ key={option.value || option.text}
164
+ value={option.value}
165
+ text={t(option.text, (option as any).display || option.text)}
166
+ />
167
+ ))}
168
+ </Select>
169
+ )}
170
+ />
171
+ );
172
+
173
+ const createTextInput = (
174
+ id: string,
175
+ name: keyof PartographyFormData,
176
+ label: string,
177
+ placeholder: string,
178
+ required = true,
179
+ ) => (
180
+ <Controller
181
+ name={name}
182
+ control={control}
183
+ rules={required ? { required: t('fieldRequired', 'This field is required') } : {}}
184
+ render={({ field, fieldState }) => (
185
+ <TextInput
186
+ id={id}
187
+ labelText={label}
188
+ placeholder={placeholder}
189
+ value={field.value || ''}
190
+ onChange={(e) => field.onChange((e.target as HTMLInputElement).value)}
191
+ invalid={!!fieldState.error}
192
+ invalidText={fieldState.error?.message}
193
+ required={required}
194
+ />
195
+ )}
196
+ />
197
+ );
198
+
199
+ const onSubmitForm = (data: PartographyFormData) => {
200
+ const processor = getGraphDataProcessor(graphType);
201
+ if (!processor.validate(data)) {
202
+ return;
203
+ }
204
+
205
+ const dataPoint = {
206
+ time: new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' }),
207
+ value: processor.getValue(data),
208
+ graphType,
209
+ admission: data.admission,
210
+ bg: data.bg,
211
+ am: data.am,
212
+ timestamp: new Date(),
213
+ ...processor.getAdditionalData(data),
214
+ };
215
+
216
+ onSubmit(dataPoint);
217
+ handleClose();
218
+ };
219
+
220
+ const handleClose = () => {
221
+ reset();
222
+ onClose();
223
+ };
224
+
225
+ const renderSpecificFields = () => {
226
+ const fieldConfigs = {
227
+ 'fetal-heart-rate': () =>
228
+ createNumberInput(
229
+ 'fetal-heart-rate-input',
230
+ 'measurementValue',
231
+ getFieldLabel('fetalHeartRate', t),
232
+ getInputRangePlaceholder('fetal-heart-rate', t),
233
+ INPUT_RANGES['fetal-heart-rate'],
234
+ ),
235
+
236
+ 'cervical-dilation': () => (
237
+ <Grid>
238
+ <Column sm={4} md={4} lg={8}>
239
+ {createNumberInput(
240
+ 'cervical-dilation-measurement',
241
+ 'measurementValue',
242
+ getFieldLabel('cervicalDilation', t),
243
+ getInputRangePlaceholder('cervical-dilation', t),
244
+ INPUT_RANGES['cervical-dilation'],
245
+ )}
246
+ </Column>
247
+ <Column sm={4} md={4} lg={8}>
248
+ {createSelect('amniotic-fluid', 'amnioticFluid', getFieldLabel('amnioticFluid', t), AMNIOTIC_FLUID_OPTIONS)}
249
+ </Column>
250
+ <Column sm={4} md={4} lg={8}>
251
+ {createSelect('moulding', 'moulding', getFieldLabel('moulding', t), MOULDING_OPTIONS)}
252
+ </Column>
253
+ </Grid>
254
+ ),
255
+
256
+ 'descent-of-head': () =>
257
+ createSelect(
258
+ 'descent-select',
259
+ 'measurementValue',
260
+ getFieldLabel('descentOfHead', t),
261
+ DESCENT_OF_HEAD_OPTIONS,
262
+ true,
263
+ ),
264
+
265
+ 'uterine-contractions': () => (
266
+ <Grid>
267
+ <Column sm={4} md={4} lg={8}>
268
+ {createNumberInput(
269
+ 'contraction-frequency',
270
+ 'measurementValue',
271
+ getFieldLabel('frequency', t),
272
+ getInputRangePlaceholder('uterine-contractions', t),
273
+ INPUT_RANGES['uterine-contractions'],
274
+ )}
275
+ </Column>
276
+ <Column sm={4} md={4} lg={8}>
277
+ {createSelect(
278
+ 'contraction-intensity',
279
+ 'eventDescription',
280
+ getFieldLabel('intensity', t),
281
+ CONTRACTION_INTENSITY_OPTIONS,
282
+ )}
283
+ </Column>
284
+ </Grid>
285
+ ),
286
+
287
+ 'maternal-pulse': () =>
288
+ createNumberInput(
289
+ 'maternal-pulse-input',
290
+ 'measurementValue',
291
+ getFieldLabel('maternalPulse', t),
292
+ getInputRangePlaceholder('maternal-pulse', t),
293
+ INPUT_RANGES['maternal-pulse'],
294
+ ),
295
+
296
+ 'blood-pressure': () => (
297
+ <Grid>
298
+ <Column sm={4} md={4} lg={8}>
299
+ {createNumberInput(
300
+ 'systolic-pressure',
301
+ 'systolic',
302
+ getFieldLabel('systolic', t),
303
+ getInputRangePlaceholder('systolic-bp', t),
304
+ INPUT_RANGES['systolic-bp'],
305
+ )}
306
+ </Column>
307
+ <Column sm={4} md={4} lg={8}>
308
+ {createNumberInput(
309
+ 'diastolic-pressure',
310
+ 'diastolic',
311
+ getFieldLabel('diastolic', t),
312
+ getInputRangePlaceholder('diastolic-bp', t),
313
+ INPUT_RANGES['diastolic-bp'],
314
+ )}
315
+ </Column>
316
+ </Grid>
317
+ ),
318
+
319
+ temperature: () =>
320
+ createNumberInput(
321
+ 'temperature-input',
322
+ 'measurementValue',
323
+ getFieldLabel('temperature', t),
324
+ getInputRangePlaceholder('temperature', t),
325
+ INPUT_RANGES.temperature,
326
+ ),
327
+
328
+ 'urine-analysis': () => (
329
+ <Grid>
330
+ <Column sm={4} md={2} lg={5}>
331
+ {createSelect('protein-level', 'proteinLevel', getFieldLabel('proteinLevel', t), URINE_LEVEL_OPTIONS)}
332
+ </Column>
333
+ <Column sm={4} md={3} lg={5}>
334
+ {createSelect('glucose-level', 'glucoseLevel', getFieldLabel('glucoseLevel', t), URINE_LEVEL_OPTIONS)}
335
+ </Column>
336
+ <Column sm={4} md={3} lg={6}>
337
+ {createSelect('ketone-level', 'ketoneLevel', getFieldLabel('ketoneLevel', t), URINE_LEVEL_OPTIONS)}
338
+ </Column>
339
+ </Grid>
340
+ ),
341
+
342
+ 'drugs-fluids': () => (
343
+ <Grid>
344
+ <Column sm={4} md={4} lg={8}>
345
+ {createTextInput(
346
+ 'medication-input',
347
+ 'medication',
348
+ getFieldLabel('medication', t),
349
+ getFieldPlaceholder('medicationType', t),
350
+ )}
351
+ </Column>
352
+ <Column sm={4} md={4} lg={8}>
353
+ {createTextInput(
354
+ 'dosage-input',
355
+ 'dosage',
356
+ getFieldLabel('dosageRate', t),
357
+ getFieldPlaceholder('dosageRate', t),
358
+ )}
359
+ </Column>
360
+ </Grid>
361
+ ),
362
+
363
+ 'progress-events': () => (
364
+ <Grid>
365
+ <Column sm={4} md={8} lg={16}>
366
+ {createSelect('event-type', 'eventType', getFieldLabel('eventType', t), EVENT_TYPE_OPTIONS, true)}
367
+ </Column>
368
+ <Column sm={4} md={8} lg={16}>
369
+ <Controller
370
+ name="eventDescription"
371
+ control={control}
372
+ rules={{ required: t('fieldRequired', 'This field is required') }}
373
+ render={({ field, fieldState }) => (
374
+ <TextArea
375
+ id="event-description"
376
+ labelText={getFieldLabel('eventDescription', t)}
377
+ placeholder={getFieldPlaceholder('eventDescription', t)}
378
+ value={field.value || ''}
379
+ onChange={(e) => field.onChange((e.target as HTMLTextAreaElement).value)}
380
+ invalid={!!fieldState.error}
381
+ invalidText={fieldState.error?.message}
382
+ rows={3}
383
+ required
384
+ />
385
+ )}
386
+ />
387
+ </Column>
388
+ </Grid>
389
+ ),
390
+ };
391
+
392
+ const config = fieldConfigs[graphType as keyof typeof fieldConfigs];
393
+ return config
394
+ ? config()
395
+ : createTextInput(
396
+ 'general-measurement',
397
+ 'measurementValue',
398
+ getMeasurementLabel(graphType, t, graphTitle),
399
+ getFieldPlaceholder('generalMeasurement', t),
400
+ );
401
+ };
402
+
403
+ return (
404
+ <Modal
405
+ open={isOpen}
406
+ onRequestClose={onClose}
407
+ modalHeading={`${getMeasurementLabel(graphType, t, graphTitle)} Data`}
408
+ modalLabel={`${patientInfo.name}, ${patientInfo.gender}, ${patientInfo.age}`}
409
+ primaryButtonText={t('save', 'Save')}
410
+ secondaryButtonText={t('cancel', 'Cancel')}
411
+ onRequestSubmit={handleSubmit(onSubmitForm)}
412
+ onSecondarySubmit={handleClose}
413
+ size="lg">
414
+ <div className={styles.modalContent}>
415
+ <Form onSubmit={handleSubmit(onSubmitForm)}>
416
+ <Grid>
417
+ <Column sm={4} md={8} lg={5}>
418
+ {createSelect('admission-select', 'admission', getFieldLabel('admission', t), ADMISSION_OPTIONS)}
419
+ </Column>
420
+ <Column sm={4} md={8} lg={5}>
421
+ {createSelect('bg-select', 'bg', getFieldLabel('bg', t), BLOOD_GROUP_OPTIONS)}
422
+ </Column>
423
+ <Column sm={4} md={8} lg={6}>
424
+ {createSelect('am-select', 'am', getFieldLabel('am', t), TIME_SLOT_OPTIONS)}
425
+ </Column>
426
+ </Grid>
427
+ <div className={styles.measurementSection}>{renderSpecificFields()}</div>
428
+ </Form>
429
+ </div>
430
+ </Modal>
431
+ );
432
+ };
433
+
434
+ export default PartographyDataForm;
@@ -0,0 +1,50 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@carbon/colors';
4
+
5
+ .modalContent {
6
+ padding: layout.$spacing-05;
7
+ }
8
+
9
+ .normalRangeInfo {
10
+ margin-bottom: layout.$spacing-04;
11
+ padding: layout.$spacing-03;
12
+ background-color: colors.$gray-10;
13
+ border-radius: layout.$spacing-02;
14
+ border: 1px solid colors.$gray-20;
15
+ }
16
+
17
+ .dataEntryForm {
18
+ margin-bottom: layout.$spacing-05;
19
+ }
20
+
21
+ .measurementSection {
22
+ margin-top: layout.$spacing-05;
23
+ margin-bottom: layout.$spacing-05;
24
+ }
25
+
26
+ .timeInputContainer {
27
+ margin-bottom: layout.$spacing-04;
28
+
29
+ .timeLabel {
30
+ @include type.type-style('label-01');
31
+ color: colors.$gray-100;
32
+ margin-bottom: layout.$spacing-02;
33
+ display: block;
34
+ }
35
+
36
+ .timeInputGroup {
37
+ display: flex;
38
+ align-items: center;
39
+ gap: layout.$spacing-02;
40
+
41
+ .timeInput {
42
+ flex: 1;
43
+ min-width: layout.$spacing-10;
44
+ }
45
+
46
+ .timePeriodSelect {
47
+ min-width: layout.$spacing-10;
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { ConfigurableLink } from '@openmrs/esm-framework';
4
+
5
+ export interface PartographLinkProps {
6
+ patientUuid: string;
7
+ }
8
+
9
+ const PartographLink: React.FC<PartographLinkProps> = ({ patientUuid }) => {
10
+ const { t } = useTranslation();
11
+
12
+ return (
13
+ <ConfigurableLink
14
+ to={`\${openmrsSpaBase}/patient/\${patientUuid}/chart/partography`}
15
+ templateParams={{ patientUuid }}>
16
+ {t('partography', 'Partography')}
17
+ </ConfigurableLink>
18
+ );
19
+ };
20
+
21
+ export default PartographLink;