@kenyaemr/esm-patient-clinical-view-app 5.4.2-pre.2823 → 5.4.2-pre.2832
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/dist/{791.js → 343.js} +1 -1
- package/dist/343.js.map +1 -0
- package/dist/kenyaemr-esm-patient-clinical-view-app.js +3 -3
- package/dist/kenyaemr-esm-patient-clinical-view-app.js.buildmanifest.json +27 -27
- package/dist/main.js +17 -17
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/maternal-and-child-health/partography/forms/cervix-form.component.tsx +60 -9
- package/src/maternal-and-child-health/partography/forms/drugs-iv-fluids-form.component.tsx +21 -17
- package/src/maternal-and-child-health/partography/forms/fetal-heart-rate-form.component.tsx +70 -6
- package/src/maternal-and-child-health/partography/forms/membrane-amniotic-fluid-form.component.tsx +30 -7
- package/src/maternal-and-child-health/partography/forms/urine-test-form.component.tsx +63 -6
- package/src/maternal-and-child-health/partography/graphs/cervix-graph.component.tsx +100 -46
- package/src/maternal-and-child-health/partography/graphs/oxytocin-graph.component.tsx +2 -1
- package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph.component.tsx +231 -133
- package/src/maternal-and-child-health/partography/partograph.component.tsx +141 -30
- package/src/maternal-and-child-health/partography/partography-data-form.scss +31 -12
- package/src/maternal-and-child-health/partography/partography.scss +22 -86
- package/src/maternal-and-child-health/partography/resources/cervix.resource.ts +15 -1
- package/src/maternal-and-child-health/partography/resources/membrane-amniotic-fluid.resource.ts +170 -1
- package/src/maternal-and-child-health/partography/resources/oxytocin.resource.ts +88 -15
- package/src/maternal-and-child-health/partography/resources/temperature.resource.ts +138 -1
- package/dist/791.js.map +0 -1
package/dist/routes.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.0.0"},"pages":[{"component":"wrapComponent","route":"case-management"}],"extensions":[{"name":"hiv-care-and-treatment-dashboard-link","component":"hivCareAndTreatmentLink","slot":"patient-chart-dashboard-slot","meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-hiv-care-and-treatment-dashboard-slot","path":"hiv-care-and-treatment-dashboard","layoutMode":"anchored"}},{"name":"hiv-care-and-treatment-dashboard","slot":"patient-chart-hiv-care-and-treatment-dashboard-slot","component":"hivCareAndTreatment","order":0,"online":true,"offline":false},{"name":"relationship-dashboard-link","component":"relationshipsLink","order":24,"online":true,"offline":false,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-relationship-slot","path":"relationships","layoutMode":"anchored"}},{"name":"relationship-dashboard","slot":"patient-chart-relationship-slot","component":"relationships","order":0,"online":true,"offline":false},{"name":"clinical-encounter-dashboard-link","component":"clinicalEncounterLink","order":25,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-clinical-encounter-slot","path":"clinical-encounter","layoutMode":"anchored"}},{"name":"clinical-encounter-dashboard","slot":"patient-chart-clinical-encounter-slot","component":"clinicalEncounter","order":0,"online":true,"offline":false},{"name":"maternal-and-child-health-dashboard-link","component":"maternalAndChildHealthDashboardLink","order":26,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-maternal-and-child-health-slot","path":"maternal-and-child-health","layoutMode":"anchored"}},{"name":"maternal-and-child-health-dashboard","slot":"patient-chart-maternal-and-child-health-slot","component":"maternalAndChildHealthDashboard","order":0,"online":true,"offline":false},{"name":"maternal-and-child-health-partograph","slot":"maternal-and-child-health-partograph-slot","component":"partograph","order":0,"online":true,"offline":false},{"name":"contact-list-dashboard-link","component":"contactListLink","order":27,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-contact-list-slot","path":"contact-list","layoutMode":"anchored"}},{"name":"contact-list-dashboard","slot":"patient-chart-contact-list-slot","component":"contactList","order":0,"online":true,"offline":false},{"component":"caseManagementDashboardLink","name":"case-management-dashboard-link","meta":{"name":"case-management","title":"Case Management","slot":"case-management-dashboard-slot","path":"/case-management"}},{"name":"wrap-component-view","slot":"case-management-dashboard-slot","component":"wrapComponent","order":2,"online":true,"offline":false},{"name":"case-encounter-link","component":"caseEncounterDashboardLink","order":14,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-case-encounter-slot","path":"case-management-encounters","layoutMode":"anchored"}},{"name":"case-encounter-table","slot":"patient-chart-case-encounter-slot","component":"caseEncounterTable","order":0,"online":true,"offline":false},{"component":"peerCalendarDashboardLink","name":"peer-calendar-dashboard-link","meta":{"name":"peer-calendar","title":"Peer Calendar","slot":"peer-calendar-dashboard-slot","path":"peer-management"}},{"name":"peer-calendar","slot":"peer-calendar-dashboard-slot","component":"peerCalendar","order":0,"online":true,"offline":false},{"name":"special-clinics-dashboard-link","component":"specialClinicsDashboardLink","order":15,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-special-clinics-slot","path":"special-clinics","layoutMode":"anchored"}},{"name":"special-clinics-dashboard","slot":"patient-chart-special-clinics-slot","component":"specialClinicsDashboard","order":0,"online":true,"offline":false},{"name":"patient-complaints","slot":"ewf-patient-summary-slot","component":"patientComplaints","order":0,"online":true,"offline":false}],"modals":[{"name":"birth-date-calculator","component":"birthDateCalculator"},{"name":"relationship-delete-confirm-dialog","component":"relationshipDeleteConfirmialog"},{"name":"end-relationship-dialog","component":"endRelationshipModal"}],"workspaces":[{"name":"contact-list-form","component":"contactListForm","title":"Contact List Form","type":"form"},{"name":"case-management-form","component":"caseManagementForm","title":"Case Management Form","type":"form"},{"name":"add-patient-case-form","component":"addPatientCaseForm","title":"Add patient case Form","type":"form"},{"name":"family-relationship-form","component":"familyRelationshipForm","title":"Family Relationship Form","type":"form"},{"name":"peers-form","component":"peersForm","title":"Add New Peer","type":"form"},{"name":"kenyaemr-cusom-form-entry-workspace","component":"peerCalendarFormEntry","title":"KVP Peer Educator Outreach Calendar","type":"form","width":"extra-wide","canMaximize":true,"canHide":true},{"name":"contact-list-update-form","component":"contactListUpdateForm","title":"Contact List Update Form","type":"form"},{"name":"other-relationship-form","component":"otherRelationshipsForm","title":"Other Relationships Form","type":"form"},{"name":"end-relationship-form","component":"endRelationshipWorkspace","title":"Discontinue relationship form","type":"form"}],"version":"5.4.2-pre.
|
|
1
|
+
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.0.0"},"pages":[{"component":"wrapComponent","route":"case-management"}],"extensions":[{"name":"hiv-care-and-treatment-dashboard-link","component":"hivCareAndTreatmentLink","slot":"patient-chart-dashboard-slot","meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-hiv-care-and-treatment-dashboard-slot","path":"hiv-care-and-treatment-dashboard","layoutMode":"anchored"}},{"name":"hiv-care-and-treatment-dashboard","slot":"patient-chart-hiv-care-and-treatment-dashboard-slot","component":"hivCareAndTreatment","order":0,"online":true,"offline":false},{"name":"relationship-dashboard-link","component":"relationshipsLink","order":24,"online":true,"offline":false,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-relationship-slot","path":"relationships","layoutMode":"anchored"}},{"name":"relationship-dashboard","slot":"patient-chart-relationship-slot","component":"relationships","order":0,"online":true,"offline":false},{"name":"clinical-encounter-dashboard-link","component":"clinicalEncounterLink","order":25,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-clinical-encounter-slot","path":"clinical-encounter","layoutMode":"anchored"}},{"name":"clinical-encounter-dashboard","slot":"patient-chart-clinical-encounter-slot","component":"clinicalEncounter","order":0,"online":true,"offline":false},{"name":"maternal-and-child-health-dashboard-link","component":"maternalAndChildHealthDashboardLink","order":26,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-maternal-and-child-health-slot","path":"maternal-and-child-health","layoutMode":"anchored"}},{"name":"maternal-and-child-health-dashboard","slot":"patient-chart-maternal-and-child-health-slot","component":"maternalAndChildHealthDashboard","order":0,"online":true,"offline":false},{"name":"maternal-and-child-health-partograph","slot":"maternal-and-child-health-partograph-slot","component":"partograph","order":0,"online":true,"offline":false},{"name":"contact-list-dashboard-link","component":"contactListLink","order":27,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-contact-list-slot","path":"contact-list","layoutMode":"anchored"}},{"name":"contact-list-dashboard","slot":"patient-chart-contact-list-slot","component":"contactList","order":0,"online":true,"offline":false},{"component":"caseManagementDashboardLink","name":"case-management-dashboard-link","meta":{"name":"case-management","title":"Case Management","slot":"case-management-dashboard-slot","path":"/case-management"}},{"name":"wrap-component-view","slot":"case-management-dashboard-slot","component":"wrapComponent","order":2,"online":true,"offline":false},{"name":"case-encounter-link","component":"caseEncounterDashboardLink","order":14,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-case-encounter-slot","path":"case-management-encounters","layoutMode":"anchored"}},{"name":"case-encounter-table","slot":"patient-chart-case-encounter-slot","component":"caseEncounterTable","order":0,"online":true,"offline":false},{"component":"peerCalendarDashboardLink","name":"peer-calendar-dashboard-link","meta":{"name":"peer-calendar","title":"Peer Calendar","slot":"peer-calendar-dashboard-slot","path":"peer-management"}},{"name":"peer-calendar","slot":"peer-calendar-dashboard-slot","component":"peerCalendar","order":0,"online":true,"offline":false},{"name":"special-clinics-dashboard-link","component":"specialClinicsDashboardLink","order":15,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-special-clinics-slot","path":"special-clinics","layoutMode":"anchored"}},{"name":"special-clinics-dashboard","slot":"patient-chart-special-clinics-slot","component":"specialClinicsDashboard","order":0,"online":true,"offline":false},{"name":"patient-complaints","slot":"ewf-patient-summary-slot","component":"patientComplaints","order":0,"online":true,"offline":false}],"modals":[{"name":"birth-date-calculator","component":"birthDateCalculator"},{"name":"relationship-delete-confirm-dialog","component":"relationshipDeleteConfirmialog"},{"name":"end-relationship-dialog","component":"endRelationshipModal"}],"workspaces":[{"name":"contact-list-form","component":"contactListForm","title":"Contact List Form","type":"form"},{"name":"case-management-form","component":"caseManagementForm","title":"Case Management Form","type":"form"},{"name":"add-patient-case-form","component":"addPatientCaseForm","title":"Add patient case Form","type":"form"},{"name":"family-relationship-form","component":"familyRelationshipForm","title":"Family Relationship Form","type":"form"},{"name":"peers-form","component":"peersForm","title":"Add New Peer","type":"form"},{"name":"kenyaemr-cusom-form-entry-workspace","component":"peerCalendarFormEntry","title":"KVP Peer Educator Outreach Calendar","type":"form","width":"extra-wide","canMaximize":true,"canHide":true},{"name":"contact-list-update-form","component":"contactListUpdateForm","title":"Contact List Update Form","type":"form"},{"name":"other-relationship-form","component":"otherRelationshipsForm","title":"Other Relationships Form","type":"form"},{"name":"end-relationship-form","component":"endRelationshipWorkspace","title":"Discontinue relationship form","type":"form"}],"version":"5.4.2-pre.2832"}
|
package/package.json
CHANGED
|
@@ -73,25 +73,49 @@ const CervixForm: React.FC<CervixFormProps> = ({
|
|
|
73
73
|
};
|
|
74
74
|
}, [existingCervixData]);
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
// Calculate the latest hour for progressive validation
|
|
77
|
+
const latestHour = useMemo(() => {
|
|
78
|
+
if (!existingTimeEntries || existingTimeEntries.length === 0) {
|
|
79
|
+
return null;
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
// Find the maximum hour from existing entries
|
|
82
|
+
const hours = existingTimeEntries.map((entry) => entry.hour);
|
|
83
|
+
return Math.max(...hours);
|
|
84
|
+
}, [existingTimeEntries]);
|
|
82
85
|
|
|
86
|
+
// Generate hour options with progressive validation
|
|
83
87
|
const hourOptions = useMemo(() => {
|
|
84
88
|
return Array.from({ length: 24 }, (_, i) => {
|
|
85
89
|
const hourValue = String(i).padStart(2, '0');
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
|
|
91
|
+
// Progressive validation: disable if hour is less than or equal to latest hour
|
|
92
|
+
const isUsed = selectedHours.includes(i);
|
|
93
|
+
const isBeforeLatest = latestHour !== null && i <= latestHour;
|
|
94
|
+
const isDisabled = isUsed || isBeforeLatest;
|
|
95
|
+
|
|
96
|
+
let displayText = hourValue;
|
|
97
|
+
if (isUsed) {
|
|
98
|
+
displayText = `${hourValue} (used)`;
|
|
99
|
+
} else if (isBeforeLatest) {
|
|
100
|
+
displayText = `${hourValue} (unavailable)`;
|
|
101
|
+
}
|
|
102
|
+
|
|
88
103
|
return {
|
|
89
104
|
value: hourValue,
|
|
90
105
|
text: displayText,
|
|
91
106
|
disabled: isDisabled,
|
|
92
107
|
};
|
|
93
108
|
});
|
|
94
|
-
}, [
|
|
109
|
+
}, [selectedHours, latestHour]);
|
|
110
|
+
|
|
111
|
+
// Helper text for user guidance
|
|
112
|
+
const getHourSelectionHelperText = () => {
|
|
113
|
+
if (latestHour === null) {
|
|
114
|
+
return t('firstEntryHelp', 'Select the hour for this first cervical measurement');
|
|
115
|
+
}
|
|
116
|
+
const nextHour = latestHour + 1;
|
|
117
|
+
return t('progressiveEntryHelp', `Select hour ${nextHour} or later (latest entry: ${latestHour})`);
|
|
118
|
+
};
|
|
95
119
|
|
|
96
120
|
// Clear any errors when form opens
|
|
97
121
|
useEffect(() => {
|
|
@@ -114,6 +138,26 @@ const CervixForm: React.FC<CervixFormProps> = ({
|
|
|
114
138
|
return;
|
|
115
139
|
}
|
|
116
140
|
|
|
141
|
+
// Progressive validation: prevent selection of hours before or equal to latest
|
|
142
|
+
const selectedHour = parseInt(data.hour);
|
|
143
|
+
if (latestHour !== null && selectedHour <= latestHour) {
|
|
144
|
+
const nextValidHour = latestHour + 1;
|
|
145
|
+
setError('hour', {
|
|
146
|
+
type: 'manual',
|
|
147
|
+
message: `Please select hour ${nextValidHour} or later. Latest entry was at hour ${latestHour}.`,
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Duplicate hour validation
|
|
153
|
+
if (existingTimeEntries.some((entry) => entry.hour === selectedHour)) {
|
|
154
|
+
setError('hour', {
|
|
155
|
+
type: 'manual',
|
|
156
|
+
message: `Hour ${selectedHour} has already been used. Please select a different hour.`,
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
117
161
|
if (!data.time || data.time === '') {
|
|
118
162
|
setError('time', { type: 'manual', message: 'Time selection is required' });
|
|
119
163
|
return;
|
|
@@ -218,13 +262,20 @@ const CervixForm: React.FC<CervixFormProps> = ({
|
|
|
218
262
|
<Select
|
|
219
263
|
id="hour-select"
|
|
220
264
|
labelText="Hour *"
|
|
265
|
+
helperText={getHourSelectionHelperText()}
|
|
221
266
|
value={field.value}
|
|
222
267
|
onChange={(e) => field.onChange((e.target as HTMLSelectElement).value)}
|
|
223
268
|
invalid={!!fieldState.error}
|
|
224
269
|
invalidText={fieldState.error?.message}>
|
|
225
270
|
<SelectItem value="" text="Select hour" />
|
|
226
271
|
{hourOptions.map((option) => (
|
|
227
|
-
<SelectItem
|
|
272
|
+
<SelectItem
|
|
273
|
+
key={option.value}
|
|
274
|
+
value={option.disabled ? '' : option.value}
|
|
275
|
+
text={option.text}
|
|
276
|
+
disabled={option.disabled}
|
|
277
|
+
className={option.disabled ? styles.disabledHourOption : ''}
|
|
278
|
+
/>
|
|
228
279
|
))}
|
|
229
280
|
</Select>
|
|
230
281
|
)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useState, useCallback, useEffect } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import { useForm, Controller } from 'react-hook-form';
|
|
4
|
-
import { Button, Modal, Grid, Column, Dropdown, TextInput, ButtonSkeleton } from '@carbon/react';
|
|
4
|
+
import { Button, Modal, Grid, Column, Dropdown, TextInput, ButtonSkeleton, Select, SelectItem } from '@carbon/react';
|
|
5
5
|
import { launchWorkspace, useSession, openmrsFetch, showSnackbar } from '@openmrs/esm-framework';
|
|
6
6
|
import { saveDrugOrderData } from '../partography.resource';
|
|
7
7
|
import styles from '../partography-data-form.scss';
|
|
@@ -283,18 +283,20 @@ const DrugsIVFluidsForm: React.FC<DrugsIVFluidsFormProps> = ({ isOpen, onClose,
|
|
|
283
283
|
<Controller
|
|
284
284
|
name="route"
|
|
285
285
|
control={control}
|
|
286
|
+
rules={{ required: t('routeRequired', 'Please select route') }}
|
|
286
287
|
render={({ field, fieldState }) => (
|
|
287
|
-
<
|
|
288
|
+
<Select
|
|
288
289
|
id="route-dropdown"
|
|
289
|
-
|
|
290
|
-
label={t('selectRoute', 'Select route')}
|
|
291
|
-
items={routeOptions}
|
|
292
|
-
itemToString={(item) => (item ? item.text : '')}
|
|
293
|
-
selectedItem={field.value ? routeOptions.find((opt) => opt.id === field.value) : null}
|
|
294
|
-
onChange={({ selectedItem }) => field.onChange(selectedItem?.id || '')}
|
|
290
|
+
labelText={t('route', 'Route')}
|
|
295
291
|
invalid={!!fieldState.error}
|
|
296
292
|
invalidText={fieldState.error?.message}
|
|
297
|
-
|
|
293
|
+
value={field.value}
|
|
294
|
+
onChange={(e) => field.onChange(e.target.value)}>
|
|
295
|
+
<SelectItem value="" text={t('chooseAnOption', 'Choose an option')} />
|
|
296
|
+
{routeOptions.map((option) => (
|
|
297
|
+
<SelectItem key={option.id} value={option.id} text={option.text} />
|
|
298
|
+
))}
|
|
299
|
+
</Select>
|
|
298
300
|
)}
|
|
299
301
|
/>
|
|
300
302
|
</Column>
|
|
@@ -303,18 +305,20 @@ const DrugsIVFluidsForm: React.FC<DrugsIVFluidsFormProps> = ({ isOpen, onClose,
|
|
|
303
305
|
<Controller
|
|
304
306
|
name="frequency"
|
|
305
307
|
control={control}
|
|
308
|
+
rules={{ required: t('frequencyRequired', 'Please select frequency') }}
|
|
306
309
|
render={({ field, fieldState }) => (
|
|
307
|
-
<
|
|
310
|
+
<Select
|
|
308
311
|
id="frequency-dropdown"
|
|
309
|
-
|
|
310
|
-
label={t('selectFrequency', 'Select frequency')}
|
|
311
|
-
items={frequencyOptions}
|
|
312
|
-
itemToString={(item) => (item ? item.text : '')}
|
|
313
|
-
selectedItem={field.value ? frequencyOptions.find((opt) => opt.id === field.value) : null}
|
|
314
|
-
onChange={({ selectedItem }) => field.onChange(selectedItem?.id || '')}
|
|
312
|
+
labelText={t('frequency', 'Frequency')}
|
|
315
313
|
invalid={!!fieldState.error}
|
|
316
314
|
invalidText={fieldState.error?.message}
|
|
317
|
-
|
|
315
|
+
value={field.value}
|
|
316
|
+
onChange={(e) => field.onChange(e.target.value)}>
|
|
317
|
+
<SelectItem value="" text={t('chooseAnOption', 'Choose an option')} />
|
|
318
|
+
{frequencyOptions.map((option) => (
|
|
319
|
+
<SelectItem key={option.id} value={option.id} text={option.text} />
|
|
320
|
+
))}
|
|
321
|
+
</Select>
|
|
318
322
|
)}
|
|
319
323
|
/>
|
|
320
324
|
</Column>
|
|
@@ -55,6 +55,21 @@ const FetalHeartRateForm: React.FC<FetalHeartRateFormProps> = ({
|
|
|
55
55
|
fetalHeartRate: '',
|
|
56
56
|
},
|
|
57
57
|
});
|
|
58
|
+
|
|
59
|
+
// Reset form when it opens
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
if (isOpen) {
|
|
62
|
+
reset({
|
|
63
|
+
hour: '',
|
|
64
|
+
time: '',
|
|
65
|
+
fetalHeartRate: '',
|
|
66
|
+
});
|
|
67
|
+
clearErrors();
|
|
68
|
+
setSaveError(null);
|
|
69
|
+
setSaveSuccess(false);
|
|
70
|
+
}
|
|
71
|
+
}, [isOpen, reset, clearErrors]);
|
|
72
|
+
|
|
58
73
|
const generateHourOptions = () => {
|
|
59
74
|
const options = [];
|
|
60
75
|
// Add 00 hour option first
|
|
@@ -73,20 +88,41 @@ const FetalHeartRateForm: React.FC<FetalHeartRateFormProps> = ({
|
|
|
73
88
|
return null;
|
|
74
89
|
}
|
|
75
90
|
// Find the max hour value from existingTimeEntries (hour is already a number)
|
|
76
|
-
|
|
91
|
+
const hours = existingTimeEntries.map((e) => {
|
|
92
|
+
// Ensure we're working with numbers, not strings
|
|
93
|
+
const hourNum = typeof e.hour === 'number' ? e.hour : parseFloat(e.hour);
|
|
94
|
+
return isNaN(hourNum) ? 0 : hourNum;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const maxHour = Math.max(...hours);
|
|
98
|
+
return maxHour;
|
|
77
99
|
}, [existingTimeEntries]);
|
|
78
100
|
|
|
79
101
|
// Generate hour options, disabling those before the latest entered hour (float comparison)
|
|
80
102
|
const hourOptionsWithDisabled = React.useMemo(() => {
|
|
81
103
|
return generateHourOptions().map((option) => {
|
|
82
104
|
const hourValue = parseFloat(option.value);
|
|
105
|
+
|
|
106
|
+
// More precise comparison - disable if hour is less than or equal to latest hour
|
|
107
|
+
// Use small epsilon to handle floating point precision issues
|
|
108
|
+
const isDisabled = latestHour !== null && hourValue <= latestHour + 0.001;
|
|
109
|
+
|
|
83
110
|
return {
|
|
84
111
|
...option,
|
|
85
|
-
disabled:
|
|
112
|
+
disabled: isDisabled,
|
|
113
|
+
displayLabel: isDisabled ? `${option.label} (used)` : option.label,
|
|
86
114
|
};
|
|
87
115
|
});
|
|
88
116
|
}, [latestHour]);
|
|
89
117
|
|
|
118
|
+
// Helper text for user guidance
|
|
119
|
+
const getHourSelectionHelperText = () => {
|
|
120
|
+
if (latestHour === null) {
|
|
121
|
+
return t('firstEntryHelp', 'Select the hour for this first measurement');
|
|
122
|
+
}
|
|
123
|
+
return t('progressiveEntryHelp', `Select an hour after ${latestHour}hr (latest entry)`);
|
|
124
|
+
};
|
|
125
|
+
|
|
90
126
|
const handleFormSubmit = async (data: FetalHeartRateFormData) => {
|
|
91
127
|
const hourValue = parseFloat(data.hour);
|
|
92
128
|
const fetalHeartRateValue = parseInt(data.fetalHeartRate);
|
|
@@ -99,6 +135,32 @@ const FetalHeartRateForm: React.FC<FetalHeartRateFormProps> = ({
|
|
|
99
135
|
setError('hour', { type: 'manual', message: t('hourRequired', 'Please select a valid hour') });
|
|
100
136
|
return;
|
|
101
137
|
}
|
|
138
|
+
|
|
139
|
+
// Prevent selection of hours before or equal to the latest entry
|
|
140
|
+
if (latestHour !== null && hourValue <= latestHour) {
|
|
141
|
+
const nextValidHour = latestHour + 0.5;
|
|
142
|
+
setError('hour', {
|
|
143
|
+
type: 'manual',
|
|
144
|
+
message: t(
|
|
145
|
+
'hourMustBeAfterLatest',
|
|
146
|
+
`Hour must be after ${latestHour}hr (latest entry). Please select ${nextValidHour}hr or later.`,
|
|
147
|
+
),
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for exact duplicate hours
|
|
153
|
+
const isDuplicateHour = existingTimeEntries.some(
|
|
154
|
+
(entry) => Math.abs(entry.hour - hourValue) < 0.001, // Handle floating point precision
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (isDuplicateHour) {
|
|
158
|
+
setError('hour', {
|
|
159
|
+
type: 'manual',
|
|
160
|
+
message: t('duplicateHour', `${hourValue}hr has already been used. Please select a different hour.`),
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
102
164
|
if (!data.time || data.time.trim() === '') {
|
|
103
165
|
setError('time', { type: 'manual', message: t('timeRequired', 'Time is required') });
|
|
104
166
|
return;
|
|
@@ -220,18 +282,20 @@ const FetalHeartRateForm: React.FC<FetalHeartRateFormProps> = ({
|
|
|
220
282
|
render={({ field, fieldState }) => (
|
|
221
283
|
<Select
|
|
222
284
|
id="hour-select"
|
|
223
|
-
labelText={t('hour', 'Hour')}
|
|
285
|
+
labelText={t('hour', 'Hour *')}
|
|
286
|
+
helperText={getHourSelectionHelperText()}
|
|
224
287
|
invalid={!!fieldState.error}
|
|
225
288
|
invalidText={fieldState.error?.message}
|
|
226
289
|
value={field.value}
|
|
227
290
|
onChange={(e) => field.onChange(e.target.value)}>
|
|
228
|
-
<SelectItem value="" text={t('
|
|
291
|
+
<SelectItem value="" text={t('selectHour', 'Select hour')} />
|
|
229
292
|
{hourOptionsWithDisabled.map((option) => (
|
|
230
293
|
<SelectItem
|
|
231
294
|
key={option.value}
|
|
232
|
-
value={option.value}
|
|
233
|
-
text={option.
|
|
295
|
+
value={option.disabled ? '' : option.value}
|
|
296
|
+
text={option.displayLabel}
|
|
234
297
|
disabled={option.disabled}
|
|
298
|
+
className={option.disabled ? styles.disabledHourOption : ''}
|
|
235
299
|
/>
|
|
236
300
|
))}
|
|
237
301
|
</Select>
|
package/src/maternal-and-child-health/partography/forms/membrane-amniotic-fluid-form.component.tsx
CHANGED
|
@@ -74,8 +74,17 @@ const MembraneAmnioticFluidForm: React.FC<MembraneAmnioticFluidFormProps> = ({
|
|
|
74
74
|
if (!existingTimeEntries || existingTimeEntries.length === 0) {
|
|
75
75
|
return null;
|
|
76
76
|
}
|
|
77
|
-
// Convert
|
|
78
|
-
const hours = existingTimeEntries
|
|
77
|
+
// Convert exactTime values to decimal hours and find the maximum
|
|
78
|
+
const hours = existingTimeEntries
|
|
79
|
+
.map((entry) => {
|
|
80
|
+
const time = entry.exactTime || '00:00';
|
|
81
|
+
if (time.match(/^\d{1,2}:\d{2}$/)) {
|
|
82
|
+
const [hours, minutes] = time.split(':').map(Number);
|
|
83
|
+
return hours + minutes / 60; // Convert to decimal hour
|
|
84
|
+
}
|
|
85
|
+
return 0;
|
|
86
|
+
})
|
|
87
|
+
.filter((hour) => hour > 0); // Filter out invalid values
|
|
79
88
|
|
|
80
89
|
return hours.length > 0 ? Math.max(...hours) : null;
|
|
81
90
|
}, [existingTimeEntries]);
|
|
@@ -108,6 +117,12 @@ const MembraneAmnioticFluidForm: React.FC<MembraneAmnioticFluidFormProps> = ({
|
|
|
108
117
|
if (!data.exactTime || data.exactTime.trim() === '') {
|
|
109
118
|
setError('exactTime', { type: 'manual', message: t('exactTimeRequired', 'Exact time is required') });
|
|
110
119
|
hasErrors = true;
|
|
120
|
+
} else if (!data.exactTime.match(/^\d{1,2}:\d{2}$/)) {
|
|
121
|
+
setError('exactTime', {
|
|
122
|
+
type: 'manual',
|
|
123
|
+
message: t('exactTimeFormat', 'Time must be in HH:MM format (e.g., 14:30)'),
|
|
124
|
+
});
|
|
125
|
+
hasErrors = true;
|
|
111
126
|
}
|
|
112
127
|
|
|
113
128
|
// Validate amniotic fluid selection
|
|
@@ -137,11 +152,12 @@ const MembraneAmnioticFluidForm: React.FC<MembraneAmnioticFluidFormProps> = ({
|
|
|
137
152
|
const selectedHour = parseFloat(data.timeSlot);
|
|
138
153
|
|
|
139
154
|
if (latestUsedHour !== null && selectedHour <= latestUsedHour) {
|
|
155
|
+
const nextValidHour = latestUsedHour + 0.5;
|
|
140
156
|
setError('timeSlot', {
|
|
141
157
|
type: 'manual',
|
|
142
158
|
message: t(
|
|
143
159
|
'timeSlotDisabled',
|
|
144
|
-
`Cannot select ${data.timeSlot}hr. Please select
|
|
160
|
+
`Cannot select ${data.timeSlot}hr. Please select ${nextValidHour}hr or later (latest entry: ${latestUsedHour}hr).`,
|
|
145
161
|
),
|
|
146
162
|
});
|
|
147
163
|
alert(t('timeSlotValidationError', 'Please select a valid time slot that comes after the previous entry.'));
|
|
@@ -259,10 +275,17 @@ const MembraneAmnioticFluidForm: React.FC<MembraneAmnioticFluidFormProps> = ({
|
|
|
259
275
|
labelText=""
|
|
260
276
|
value={field.value}
|
|
261
277
|
onChange={field.onChange}
|
|
262
|
-
existingTimeEntries={existingTimeEntries.map((e) =>
|
|
263
|
-
|
|
264
|
-
time
|
|
265
|
-
|
|
278
|
+
existingTimeEntries={existingTimeEntries.map((e) => {
|
|
279
|
+
// Convert exactTime to proper hour and time format for TimePickerDropdown
|
|
280
|
+
const time = e.exactTime || '00:00';
|
|
281
|
+
const [hours, minutes] = time.split(':').map(Number);
|
|
282
|
+
const hourValue = hours + minutes / 60; // Convert to decimal hour
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
hour: hourValue,
|
|
286
|
+
time: time,
|
|
287
|
+
};
|
|
288
|
+
})}
|
|
266
289
|
invalid={!!fieldState.error}
|
|
267
290
|
invalidText={fieldState.error?.message}
|
|
268
291
|
/>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import { useForm, Controller } from 'react-hook-form';
|
|
4
4
|
import { Button, Modal, Grid, Column, Select, SelectItem, NumberInput } from '@carbon/react';
|
|
5
|
+
import { type OpenmrsResource } from '@openmrs/esm-framework';
|
|
5
6
|
import TimePickerDropdown from './time-picker-dropdown.component';
|
|
6
7
|
import styles from '../partography-data-form.scss';
|
|
7
8
|
|
|
@@ -26,13 +27,18 @@ type UrineTestFormProps = {
|
|
|
26
27
|
timeResultsReturned: string;
|
|
27
28
|
}) => void;
|
|
28
29
|
onDataSaved?: () => void;
|
|
29
|
-
|
|
30
|
+
encounters: OpenmrsResource[];
|
|
31
|
+
isLoading?: boolean;
|
|
32
|
+
error?: Error | null;
|
|
30
33
|
patient?: {
|
|
31
34
|
uuid: string;
|
|
32
35
|
name: string;
|
|
33
36
|
gender: string;
|
|
34
37
|
age: string;
|
|
35
38
|
};
|
|
39
|
+
// Add time arrays for progressive validation
|
|
40
|
+
sampleCollectedTimes: string[];
|
|
41
|
+
resultsReturnedTimes: string[];
|
|
36
42
|
};
|
|
37
43
|
|
|
38
44
|
const UrineTestForm: React.FC<UrineTestFormProps> = ({
|
|
@@ -40,17 +46,24 @@ const UrineTestForm: React.FC<UrineTestFormProps> = ({
|
|
|
40
46
|
onClose,
|
|
41
47
|
onSubmit,
|
|
42
48
|
onDataSaved,
|
|
43
|
-
|
|
49
|
+
encounters,
|
|
50
|
+
isLoading = false,
|
|
51
|
+
error = null,
|
|
44
52
|
patient,
|
|
53
|
+
sampleCollectedTimes,
|
|
54
|
+
resultsReturnedTimes,
|
|
45
55
|
}) => {
|
|
46
56
|
const { t } = useTranslation();
|
|
47
57
|
|
|
58
|
+
// Time arrays are now passed as props from parent component
|
|
59
|
+
|
|
48
60
|
const {
|
|
49
61
|
control,
|
|
50
62
|
handleSubmit,
|
|
51
63
|
reset,
|
|
52
64
|
setError,
|
|
53
65
|
clearErrors,
|
|
66
|
+
watch,
|
|
54
67
|
formState: { errors },
|
|
55
68
|
} = useForm<UrineTestFormData>({
|
|
56
69
|
defaultValues: {
|
|
@@ -62,9 +75,42 @@ const UrineTestForm: React.FC<UrineTestFormProps> = ({
|
|
|
62
75
|
},
|
|
63
76
|
});
|
|
64
77
|
|
|
78
|
+
// Watch both time fields for cross-validation
|
|
79
|
+
const watchedTimeSampleCollected = watch('timeSampleCollected');
|
|
80
|
+
const watchedTimeResultsReturned = watch('timeResultsReturned');
|
|
81
|
+
|
|
82
|
+
// Generate SEPARATE time entries for each field's independent progressive validation
|
|
83
|
+
const sampleCollectedTimeEntries = useMemo(() => {
|
|
84
|
+
return sampleCollectedTimes.map((time) => {
|
|
85
|
+
const [hours] = time.split(':').map(Number);
|
|
86
|
+
return {
|
|
87
|
+
hour: hours,
|
|
88
|
+
time: time,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}, [sampleCollectedTimes]);
|
|
92
|
+
|
|
93
|
+
const resultsReturnedTimeEntries = useMemo(() => {
|
|
94
|
+
return resultsReturnedTimes.map((time) => {
|
|
95
|
+
const [hours] = time.split(':').map(Number);
|
|
96
|
+
return {
|
|
97
|
+
hour: hours,
|
|
98
|
+
time: time,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}, [resultsReturnedTimes]);
|
|
102
|
+
|
|
103
|
+
// Helper function to convert time string to minutes for comparison
|
|
104
|
+
const timeToMinutes = (timeString: string): number => {
|
|
105
|
+
const [hours, minutes] = timeString.split(':').map(Number);
|
|
106
|
+
return hours * 60 + minutes;
|
|
107
|
+
};
|
|
108
|
+
|
|
65
109
|
const onSubmitForm = async (data: UrineTestFormData) => {
|
|
66
110
|
const volumeValue = parseFloat(data.volume);
|
|
67
111
|
clearErrors();
|
|
112
|
+
|
|
113
|
+
// Basic field validation
|
|
68
114
|
if (!data.protein) {
|
|
69
115
|
setError('protein', {
|
|
70
116
|
type: 'manual',
|
|
@@ -101,6 +147,18 @@ const UrineTestForm: React.FC<UrineTestFormProps> = ({
|
|
|
101
147
|
return;
|
|
102
148
|
}
|
|
103
149
|
|
|
150
|
+
// Cross-validation: Results time must be >= sample collection time
|
|
151
|
+
const sampleTime = timeToMinutes(data.timeSampleCollected);
|
|
152
|
+
const resultsTime = timeToMinutes(data.timeResultsReturned);
|
|
153
|
+
|
|
154
|
+
if (resultsTime < sampleTime) {
|
|
155
|
+
setError('timeResultsReturned', {
|
|
156
|
+
type: 'manual',
|
|
157
|
+
message: t('timeResultsInvalid', 'Results return time must be after or equal to sample collection time'),
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
104
162
|
onSubmit({
|
|
105
163
|
timeSlot: data.timeSampleCollected,
|
|
106
164
|
exactTime: data.timeSampleCollected,
|
|
@@ -182,7 +240,7 @@ const UrineTestForm: React.FC<UrineTestFormProps> = ({
|
|
|
182
240
|
onChange={field.onChange}
|
|
183
241
|
invalid={!!errors.timeSampleCollected}
|
|
184
242
|
invalidText={errors.timeSampleCollected?.message}
|
|
185
|
-
existingTimeEntries={
|
|
243
|
+
existingTimeEntries={sampleCollectedTimeEntries}
|
|
186
244
|
/>
|
|
187
245
|
)}
|
|
188
246
|
/>
|
|
@@ -200,7 +258,6 @@ const UrineTestForm: React.FC<UrineTestFormProps> = ({
|
|
|
200
258
|
onChange={(e) => field.onChange(e.target.value)}
|
|
201
259
|
invalid={!!errors.acetone}
|
|
202
260
|
invalidText={errors.acetone?.message}>
|
|
203
|
-
<SelectItem value="" text={t('selectAcetone', 'Pull from Lab module')} />
|
|
204
261
|
{acetoneOptions.map((option) => (
|
|
205
262
|
<SelectItem key={option.value} value={option.value} text={option.label} />
|
|
206
263
|
))}
|
|
@@ -242,7 +299,7 @@ const UrineTestForm: React.FC<UrineTestFormProps> = ({
|
|
|
242
299
|
onChange={field.onChange}
|
|
243
300
|
invalid={!!errors.timeResultsReturned}
|
|
244
301
|
invalidText={errors.timeResultsReturned?.message}
|
|
245
|
-
existingTimeEntries={
|
|
302
|
+
existingTimeEntries={resultsReturnedTimeEntries}
|
|
246
303
|
/>
|
|
247
304
|
)}
|
|
248
305
|
/>
|