@kenyaemr/esm-billing-app 5.4.1-pre.2009 → 5.4.1-pre.2014

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.
@@ -15,7 +15,7 @@ import {
15
15
  } from '@carbon/react';
16
16
  import { zodResolver } from '@hookform/resolvers/zod';
17
17
  import { navigate, showSnackbar, useConfig, useSession } from '@openmrs/esm-framework';
18
- import React, { useEffect, useMemo, useState } from 'react';
18
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
19
19
  import { Controller, FormProvider, useForm } from 'react-hook-form';
20
20
  import { useTranslation } from 'react-i18next';
21
21
  import { useParams } from 'react-router-dom';
@@ -29,26 +29,39 @@ import useProvider from '../../../hooks/useProvider';
29
29
  import { LineItem, MappedBill } from '../../../types';
30
30
  import ClaimExplanationAndJusificationInput from './claims-explanation-and-justification-form-input.component';
31
31
  import { processClaims, SHAPackagesAndInterventionVisitAttribute, useVisit } from './claims-form.resource';
32
+
32
33
  import styles from './claims-form.scss';
34
+ import debounce from 'lodash-es/debounce';
33
35
 
34
36
  type ClaimsFormProps = {
35
37
  bill: MappedBill;
36
38
  selectedLineItems: LineItem[];
37
39
  };
38
40
 
41
+ const ClaimsFormSchemaBase = z.object({
42
+ claimExplanation: z.string(),
43
+ claimJustification: z.string(),
44
+ diagnoses: z.array(z.string()),
45
+ visitType: z.string(),
46
+ facility: z.string(),
47
+ treatmentStart: z.string(),
48
+ treatmentEnd: z.string(),
49
+ packages: z.array(z.string()),
50
+ interventions: z.array(z.string()),
51
+ provider: z.string(),
52
+ });
53
+
39
54
  const ClaimsFormSchema = z.object({
40
- claimExplanation: z.string().nonempty({ message: 'Claim explanation is required' }),
41
- claimJustification: z.string().nonempty({ message: 'Claim justification is required' }),
42
- diagnoses: z.array(z.string()).nonempty({ message: 'At least one diagnosis is required' }),
43
- visitType: z.string().nonempty({ message: 'Visit type is required' }),
44
- facility: z.string().nonempty({ message: 'Facility is required' }),
45
- treatmentStart: z.string().nonempty({ message: 'Treatment start date is required' }),
46
- treatmentEnd: z.string().nonempty({ message: 'Treatment end date is required' }),
47
- packages: z.array(z.string()).nonempty({ message: 'At least one package is required' }),
48
- interventions: z.array(z.string()).min(1, {
49
- message: 'At least one intervention is required',
50
- }),
51
- provider: z.string().nonempty({ message: 'provider is provider' }),
55
+ claimExplanation: z.string().min(1, { message: 'Claim explanation is required' }),
56
+ claimJustification: z.string().min(1, { message: 'Claim justification is required' }),
57
+ diagnoses: z.array(z.string()).min(1, { message: 'At least one diagnosis is required' }),
58
+ visitType: z.string().min(1, { message: 'Visit type is required' }),
59
+ facility: z.string().min(1, { message: 'Facility is required' }),
60
+ treatmentStart: z.string().min(1, { message: 'Treatment start date is required' }),
61
+ treatmentEnd: z.string().min(1, { message: 'Treatment end date is required' }),
62
+ packages: z.array(z.string()).min(1, { message: 'At least one package is required' }),
63
+ interventions: z.array(z.string()).min(1, { message: 'At least one intervention is required' }),
64
+ provider: z.string().min(1, { message: 'Provider is required' }),
52
65
  });
53
66
 
54
67
  const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
@@ -62,6 +75,7 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
62
75
  } = useSession();
63
76
  const { providerLoading: providerLoading, provider, error: providerError } = useProvider(providerUuid);
64
77
  const { visitAttributeTypes } = useConfig<BillingConfig>();
78
+
65
79
  const packagesAndinterventions = useMemo(() => {
66
80
  if (recentVisit) {
67
81
  const values = recentVisit.attributes?.find(
@@ -77,9 +91,9 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
77
91
 
78
92
  const encounterUuid = recentVisit?.encounters[0]?.uuid;
79
93
  const visitTypeUuid = recentVisit?.visitType.uuid;
80
-
81
94
  const [loading, setLoading] = useState(false);
82
- const { user } = useSession();
95
+ const [formInitialized, setFormInitialized] = useState(false);
96
+ const [validationEnabled, setValidationEnabled] = useState(false);
83
97
 
84
98
  const handleNavigateToBillingOptions = () =>
85
99
  navigate({
@@ -87,31 +101,92 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
87
101
  });
88
102
 
89
103
  const form = useForm<z.infer<typeof ClaimsFormSchema>>({
90
- mode: 'all',
91
- resolver: zodResolver(ClaimsFormSchema),
104
+ mode: 'onTouched',
105
+ resolver: zodResolver(validationEnabled ? ClaimsFormSchema : ClaimsFormSchemaBase),
92
106
  defaultValues: {
93
107
  claimExplanation: '',
94
108
  claimJustification: '',
95
109
  diagnoses: [],
96
- visitType: recentVisit?.visitType?.display || '',
97
- facility: `${recentVisit?.location?.display || ''} - ${mflCodeValue || ''}`,
98
- treatmentStart: recentVisit?.startDatetime ? formatDate(recentVisit.startDatetime) : '',
99
- treatmentEnd: recentVisit?.stopDatetime ? formatDate(recentVisit.stopDatetime) : '',
110
+ visitType: '',
111
+ facility: '',
112
+ treatmentStart: '',
113
+ treatmentEnd: '',
100
114
  packages: [],
101
115
  interventions: [],
102
- provider: providerUuid,
116
+ provider: '',
103
117
  },
104
118
  });
105
119
 
106
120
  const {
107
121
  control,
108
122
  handleSubmit,
109
- formState: { errors, isValid },
123
+ formState: { errors, isValid, isDirty, touchedFields },
110
124
  setValue,
111
125
  reset,
126
+ trigger,
127
+ watch,
112
128
  } = form;
113
129
 
130
+ const packages = watch('packages');
131
+ const interventions = watch('interventions');
132
+ const claimExplanation = watch('claimExplanation');
133
+ const claimJustification = watch('claimJustification');
134
+
135
+ const debouncedValidation = useCallback(
136
+ debounce(() => {
137
+ if (formInitialized) {
138
+ trigger();
139
+ }
140
+ }, 500),
141
+ [formInitialized, trigger],
142
+ );
143
+
144
+ useEffect(() => {
145
+ debouncedValidation();
146
+ return () => debouncedValidation.cancel();
147
+ }, [packages, interventions, debouncedValidation]);
148
+
149
+ useEffect(() => {
150
+ if (!visitLoading && !diagnosisLoading && !providerLoading) {
151
+ const updates = {
152
+ diagnoses: diagnoses?.map((d) => d.id) ?? [],
153
+ visitType: recentVisit?.visitType?.display || '',
154
+ facility: `${recentVisit?.location?.display || ''} - ${mflCodeValue || ''}`,
155
+ treatmentStart: recentVisit?.startDatetime ? formatDate(recentVisit.startDatetime) : '',
156
+ treatmentEnd: recentVisit?.stopDatetime ? formatDate(recentVisit.stopDatetime) : '',
157
+ packages: packagesAndinterventions?.packages ?? [],
158
+ interventions: packagesAndinterventions?.interventions ?? [],
159
+ provider: providerUuid,
160
+ };
161
+
162
+ Object.entries(updates).forEach(([field, value]) => {
163
+ setValue(field as any, value, { shouldValidate: false, shouldDirty: false, shouldTouch: false });
164
+ });
165
+
166
+ setTimeout(() => {
167
+ setFormInitialized(true);
168
+ }, 100);
169
+ }
170
+ }, [
171
+ diagnoses,
172
+ recentVisit,
173
+ mflCodeValue,
174
+ setValue,
175
+ provider,
176
+ packagesAndinterventions,
177
+ visitLoading,
178
+ diagnosisLoading,
179
+ providerLoading,
180
+ providerUuid,
181
+ ]);
182
+
114
183
  const onSubmit = async (data: z.infer<typeof ClaimsFormSchema>) => {
184
+ setValidationEnabled(true);
185
+ const isFormValid = await trigger();
186
+ if (!isFormValid) {
187
+ return;
188
+ }
189
+
115
190
  setLoading(true);
116
191
  const providedItems = selectedLineItems.reduce((acc, item) => {
117
192
  acc[item.uuid] = {
@@ -127,6 +202,7 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
127
202
  };
128
203
  return acc;
129
204
  }, {});
205
+
130
206
  const payload = {
131
207
  providedItems,
132
208
  claimExplanation: data.claimExplanation,
@@ -149,6 +225,7 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
149
225
  packages: data.packages,
150
226
  interventions: data.interventions,
151
227
  };
228
+
152
229
  try {
153
230
  await processClaims(payload);
154
231
  showSnackbar({
@@ -158,18 +235,17 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
158
235
  timeoutInMs: 3000,
159
236
  isLowContrast: true,
160
237
  });
161
- reset();
162
238
  setTimeout(() => {
163
239
  navigate({
164
- to: window.getOpenmrsSpaBase() + `home/billing/`,
240
+ to: window.getOpenmrsSpaBase() + 'home/billing/',
165
241
  });
166
- }, 2000);
242
+ }, 1000);
167
243
  } catch (err) {
168
244
  console.error(err);
169
245
  showSnackbar({
170
246
  kind: 'error',
171
247
  title: t('claimError', 'Claim Error'),
172
- subtitle: t('sendClaimError', 'Request Failed, Please try later........'),
248
+ subtitle: t('sendClaimError', 'Request Failed, Please try later...'),
173
249
  timeoutInMs: 2500,
174
250
  isLowContrast: true,
175
251
  });
@@ -178,16 +254,6 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
178
254
  }
179
255
  };
180
256
 
181
- useEffect(() => {
182
- setValue('diagnoses', diagnoses?.map((d) => d.id) ?? ([] as any));
183
- setValue('visitType', recentVisit?.visitType?.display || '');
184
- setValue('facility', `${recentVisit?.location?.display || ''} - ${mflCodeValue || ''}`);
185
- setValue('treatmentStart', recentVisit?.startDatetime ? formatDate(recentVisit.startDatetime) : '');
186
- setValue('treatmentEnd', recentVisit?.stopDatetime ? formatDate(recentVisit.stopDatetime) : '');
187
- setValue('packages', (packagesAndinterventions?.packages ?? []) as any);
188
- setValue('interventions', (packagesAndinterventions?.interventions ?? []) as any);
189
- }, [diagnoses, recentVisit, mflCodeValue, setValue, provider, packagesAndinterventions]);
190
-
191
257
  if (visitLoading || diagnosisLoading || providerLoading) {
192
258
  return (
193
259
  <Layer className={styles.loading}>
@@ -207,16 +273,33 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
207
273
  visitError?.message ??
208
274
  diagnosisError?.message ??
209
275
  providerError?.message ??
210
- 'Error occured while loading claims form'
276
+ 'Error occurred while loading claims form'
211
277
  }
212
278
  lowContrast
213
279
  />
214
280
  </Layer>
215
281
  );
216
282
  }
283
+
284
+ const shouldShowError = (fieldName: string) => {
285
+ return validationEnabled && errors[fieldName] && (touchedFields[fieldName] || formInitialized);
286
+ };
287
+
217
288
  return (
218
289
  <FormProvider {...form}>
219
290
  <Form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
291
+ {!selectedLineItems?.length && (
292
+ <div className={styles.notificationContainer}>
293
+ <InlineNotification
294
+ kind="info"
295
+ title={t('noItemsSelected', 'No items selected')}
296
+ subtitle={t('pleaseSelectItems', 'Please select line items to raise a claim')}
297
+ hideCloseButton={true}
298
+ lowContrast={true}
299
+ className={styles.notification}
300
+ />
301
+ </div>
302
+ )}
220
303
  <Stack gap={4} className={styles.grid}>
221
304
  <span className={styles.claimFormTitle}>{t('formTitle', 'Fill in the form details')}</span>
222
305
  <Row className={styles.formClaimRow}>
@@ -295,19 +378,23 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
295
378
  <Column>
296
379
  <Layer className={styles.input}>
297
380
  <Controller
298
- control={form.control}
381
+ control={control}
299
382
  name="diagnoses"
300
383
  render={({ field }) => (
301
384
  <MultiSelect
302
385
  ref={field.ref}
303
- invalid={form.formState.errors[field.name]?.message}
304
- invalidText={form.formState.errors[field.name]?.message}
386
+ invalid={shouldShowError('diagnoses')}
387
+ invalidText={errors.diagnoses?.message}
305
388
  id="diagnoses"
306
389
  titleText={t('finalDiagnosis', 'Final Diagnosis')}
307
390
  selectedItems={field.value}
308
391
  label="Choose option"
309
392
  items={diagnoses.map((r) => r.id)}
310
393
  itemToString={(item) => diagnoses.find((r) => r.id === item)?.text ?? ''}
394
+ onChange={(e) => {
395
+ field.onChange(e.selectedItems);
396
+ setValidationEnabled(true);
397
+ }}
311
398
  />
312
399
  )}
313
400
  />
@@ -322,14 +409,15 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
322
409
  render={({ field }) => (
323
410
  <Dropdown
324
411
  ref={field.ref}
325
- invalid={form.formState.errors[field.name]?.message}
326
- invalidText={form.formState.errors[field.name]?.message}
412
+ invalid={shouldShowError('provider')}
413
+ invalidText={errors.provider?.message}
327
414
  id="provider"
328
415
  titleText={t('provider', 'Provider')}
329
416
  onChange={(e) => {
330
417
  field.onChange(e.selectedItem);
418
+ setValidationEnabled(true);
331
419
  }}
332
- initialSelectedItem={field.value}
420
+ initialSelectedItem={provider.uuid}
333
421
  label="Choose option"
334
422
  items={[provider].map((r) => r.uuid)}
335
423
  itemToString={(item) =>
@@ -345,16 +433,31 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
345
433
  </Layer>
346
434
  </Column>
347
435
  </Row>
348
- <ClaimExplanationAndJusificationInput patientUuid={patientUuid} />
436
+ <ClaimExplanationAndJusificationInput
437
+ patientUuid={patientUuid}
438
+ disabled={!packages?.length || !interventions?.length}
439
+ validationEnabled={validationEnabled}
440
+ onInteraction={() => setValidationEnabled(true)}
441
+ />
349
442
  <ButtonSet className={styles.buttonSet}>
350
443
  <Button className={styles.button} kind="secondary" onClick={handleNavigateToBillingOptions}>
351
444
  {t('discardClaim', 'Discard Claim')}
352
445
  </Button>
353
- <Button className={styles.button} kind="primary" type="submit" disabled={loading}>
446
+ <Button
447
+ className={styles.button}
448
+ kind="primary"
449
+ type="submit"
450
+ onClick={() => setValidationEnabled(true)}
451
+ disabled={
452
+ loading || !isValid || !packages?.length || !interventions?.length || !selectedLineItems?.length
453
+ }
454
+ tooltipPosition="top"
455
+ tooltipAlignment="center"
456
+ renderIcon={loading ? InlineLoading : undefined}>
354
457
  {loading ? (
355
458
  <InlineLoading description={t('processing', 'Processing...')} />
356
459
  ) : (
357
- t('processClaim', 'Process Claim')
460
+ <>{t('processClaim', 'Process Claim')}</>
358
461
  )}
359
462
  </Button>
360
463
  </ButtonSet>
@@ -18,7 +18,9 @@
18
18
  color: colors.$gray-100;
19
19
  }
20
20
  }
21
-
21
+ .notificationContainer {
22
+ margin: layout.$spacing-03;
23
+ }
22
24
  .form {
23
25
  width: 100%;
24
26
  display: flex;
@@ -173,6 +173,7 @@
173
173
  "noBillsFoundDescription": "No bills found for this patient",
174
174
  "noCashPoints": "No Cash Points",
175
175
  "noCashPointsConfigured": "There are no cash points configured for this location",
176
+ "noItemsSelected": "No items selected",
176
177
  "noMatchingBillsToDisplay": "No matching bills to display",
177
178
  "noMatchingItemsToDisplay": "No matching items to display",
178
179
  "noPaymentModes": "No payment modes found",
@@ -211,6 +212,7 @@
211
212
  "payments": "Payments",
212
213
  "paymentType": "Payment Type",
213
214
  "Phone Number": "Phone Number",
215
+ "pleaseSelectItems": "Please select line items to raise a claim",
214
216
  "policyNumber": "Policy number",
215
217
  "preathsRequests": "Preauth Requests",
216
218
  "preauthRequest": "Preauth requests",