@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.
Files changed (25) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/dist/{791.js → 343.js} +1 -1
  3. package/dist/343.js.map +1 -0
  4. package/dist/kenyaemr-esm-patient-clinical-view-app.js +3 -3
  5. package/dist/kenyaemr-esm-patient-clinical-view-app.js.buildmanifest.json +27 -27
  6. package/dist/main.js +17 -17
  7. package/dist/main.js.map +1 -1
  8. package/dist/routes.json +1 -1
  9. package/package.json +1 -1
  10. package/src/maternal-and-child-health/partography/forms/cervix-form.component.tsx +60 -9
  11. package/src/maternal-and-child-health/partography/forms/drugs-iv-fluids-form.component.tsx +21 -17
  12. package/src/maternal-and-child-health/partography/forms/fetal-heart-rate-form.component.tsx +70 -6
  13. package/src/maternal-and-child-health/partography/forms/membrane-amniotic-fluid-form.component.tsx +30 -7
  14. package/src/maternal-and-child-health/partography/forms/urine-test-form.component.tsx +63 -6
  15. package/src/maternal-and-child-health/partography/graphs/cervix-graph.component.tsx +100 -46
  16. package/src/maternal-and-child-health/partography/graphs/oxytocin-graph.component.tsx +2 -1
  17. package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph.component.tsx +231 -133
  18. package/src/maternal-and-child-health/partography/partograph.component.tsx +141 -30
  19. package/src/maternal-and-child-health/partography/partography-data-form.scss +31 -12
  20. package/src/maternal-and-child-health/partography/partography.scss +22 -86
  21. package/src/maternal-and-child-health/partography/resources/cervix.resource.ts +15 -1
  22. package/src/maternal-and-child-health/partography/resources/membrane-amniotic-fluid.resource.ts +170 -1
  23. package/src/maternal-and-child-health/partography/resources/oxytocin.resource.ts +88 -15
  24. package/src/maternal-and-child-health/partography/resources/temperature.resource.ts +138 -1
  25. 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.2823"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenyaemr/esm-patient-clinical-view-app",
3
- "version": "5.4.2-pre.2823",
3
+ "version": "5.4.2-pre.2832",
4
4
  "description": "Patient clinical view microfrontend for the OpenMRS SPA",
5
5
  "keywords": [
6
6
  "openmrs"
@@ -73,25 +73,49 @@ const CervixForm: React.FC<CervixFormProps> = ({
73
73
  };
74
74
  }, [existingCervixData]);
75
75
 
76
- const usedHours = useMemo(() => {
77
- if (!selectedHours || selectedHours.length === 0) {
78
- return [];
76
+ // Calculate the latest hour for progressive validation
77
+ const latestHour = useMemo(() => {
78
+ if (!existingTimeEntries || existingTimeEntries.length === 0) {
79
+ return null;
79
80
  }
80
- return selectedHours;
81
- }, [selectedHours]);
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
- const isDisabled = usedHours.includes(i);
87
- const displayText = isDisabled ? `${hourValue} (used)` : hourValue;
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
- }, [usedHours]);
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 key={option.value} value={option.value} text={option.text} disabled={option.disabled} />
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
- <Dropdown
288
+ <Select
288
289
  id="route-dropdown"
289
- titleText={t('route', 'Route')}
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
- <Dropdown
310
+ <Select
308
311
  id="frequency-dropdown"
309
- titleText={t('frequency', 'Frequency')}
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
- return Math.max(...existingTimeEntries.map((e) => e.hour));
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: latestHour !== null && hourValue <= latestHour,
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('admissionTime', 'Admission')} />
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.label}
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>
@@ -74,8 +74,17 @@ const MembraneAmnioticFluidForm: React.FC<MembraneAmnioticFluidFormProps> = ({
74
74
  if (!existingTimeEntries || existingTimeEntries.length === 0) {
75
75
  return null;
76
76
  }
77
- // Convert timeSlot values to numeric hours and find the maximum
78
- const hours = existingTimeEntries.map((entry) => parseFloat(entry.timeSlot || '0')).filter((hour) => !isNaN(hour)); // Filter out invalid values
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 a time after ${latestUsedHour}hr.`,
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
- hour: parseInt((e.exactTime || '00:00').split(':')[0] || '0', 10) || 0,
264
- time: e.exactTime || '',
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
- existingTimeEntries?: Array<{ hour: number; time: string }>;
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
- existingTimeEntries = [],
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={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={existingTimeEntries}
302
+ existingTimeEntries={resultsReturnedTimeEntries}
246
303
  />
247
304
  )}
248
305
  />