@kenyaemr/esm-billing-app 5.4.4-pre.430 → 5.4.4-pre.435

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.
@@ -39,11 +39,17 @@ import {
39
39
  launchWorkspace2,
40
40
  restBaseUrl,
41
41
  useVisit,
42
+ showModal,
43
+ showSnackbar,
42
44
  } from '@openmrs/esm-framework';
43
45
  import { BillingConfig } from '../../config-schema';
44
46
  import { useCurrencyFormatting } from '../../helpers/currency';
45
47
  import { TFunction } from 'i18next';
46
- import { useClaimAttachments } from './claim-workspaces/attachements/claim-attachments-resource';
48
+ import {
49
+ useClaimAttachments,
50
+ retireClaimAttachment,
51
+ moveClaimToResubmit,
52
+ } from './claim-workspaces/attachements/claim-attachments-resource';
47
53
  import { useClaimDoctors } from './claim-workspaces/doctors/claim-doctors-resource';
48
54
 
49
55
  interface ClaimsMainProps {
@@ -242,7 +248,6 @@ const ClaimsTable: React.FC<{
242
248
  const totalItems = claims.length;
243
249
  const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
244
250
 
245
- // Keep the page in range when the claim set shrinks (e.g. after a sync/mutate).
246
251
  useEffect(() => {
247
252
  if (currentPage > totalPages) {
248
253
  setCurrentPage(totalPages);
@@ -287,6 +292,7 @@ const ClaimsTable: React.FC<{
287
292
  interventions: (claim.interventions ?? []).map((i) => i.intervention_code),
288
293
  paymentMechanism: claim.interventions?.[0]?.payment_mechanism,
289
294
  isResubmission: isResubmit,
295
+ providerWorkflowState: claim.provider_workflow_state,
290
296
  totalAmount,
291
297
  mutate,
292
298
  },
@@ -452,6 +458,33 @@ const ClaimDetailsPanel: React.FC<{
452
458
  mutateAttachments();
453
459
  mutateDoctors();
454
460
  }, [mutate, mutateAttachments, mutateDoctors]);
461
+ const isEditable = claim.provider_workflow_state === 'DRAFT' || claim.provider_workflow_state === 'DRAFT_RESUBMIT';
462
+ const canMoveToResubmit = claim.provider_workflow_state === 'SUBMITTED' && claim.payer_workflow_state === 'SENT_BACK';
463
+ const handleMoveToResubmit = useCallback(async () => {
464
+ const result = await moveClaimToResubmit({
465
+ consentToken: claim.authorization_code,
466
+ t,
467
+ });
468
+ if (!result.ok) {
469
+ showSnackbar({
470
+ title: t('moveToResubmitFailed', 'Could not open claim for changes'),
471
+ subtitle: result.error,
472
+ kind: 'error',
473
+ });
474
+ return;
475
+ }
476
+ combinedMutate();
477
+ showSnackbar({
478
+ title: result.alreadyOpenForEdit
479
+ ? t('claimAlreadyOpen', 'Claim is already open for changes')
480
+ : t('claimOpenedForChanges', 'Claim opened for changes'),
481
+ subtitle: t(
482
+ 'claimOpenedForChangesSubtitle',
483
+ 'You can now replace or add attachments, edit lines, and submit back to SHA when ready.',
484
+ ),
485
+ kind: 'success',
486
+ });
487
+ }, [claim.authorization_code, combinedMutate, t]);
455
488
 
456
489
  const preauthsByIntervention = useMemo(() => {
457
490
  const map = new Map<string, Array<(typeof claim.preauths)[number]>>();
@@ -510,6 +543,85 @@ const ClaimDetailsPanel: React.FC<{
510
543
  [claim.authorization_code, attachmentsByCode, combinedMutate, t],
511
544
  );
512
545
 
546
+ const handleReplaceAttachment = useCallback(
547
+ (iv: PatientClaimIntervention, attachment: { uuid: string; document_type: string; filename?: string }) => {
548
+ const dispose = showModal('replace-claim-attachment-modal', {
549
+ documentType: attachment.document_type,
550
+ filename: attachment.filename,
551
+ onConfirm: async () => {
552
+ const retireResult = await retireClaimAttachment({
553
+ consentToken: claim.authorization_code,
554
+ attachmentUuid: attachment.uuid,
555
+ interventionCode: iv.intervention_code,
556
+ t,
557
+ });
558
+ if (!retireResult.ok) {
559
+ showSnackbar({
560
+ title: t('retireFailed', 'Could not retire attachment'),
561
+ subtitle: retireResult.error,
562
+ kind: 'error',
563
+ });
564
+ return;
565
+ }
566
+
567
+ combinedMutate();
568
+
569
+ showSnackbar({
570
+ title: t('attachmentRetired', 'Attachment retired'),
571
+ subtitle: t(
572
+ 'attachmentRetiredSubtitle',
573
+ 'SHA has cleared the previous {{type}}. Upload the replacement to complete.',
574
+ { type: attachment.document_type.replace(/_/g, ' ') },
575
+ ),
576
+ kind: 'info',
577
+ });
578
+
579
+ launchWorkspace2(
580
+ 'claim-attachments-workspace',
581
+ {
582
+ workspaceTitle: t('uploadReplacementAttachment', 'Upload replacement attachment'),
583
+ consentToken: claim.authorization_code,
584
+ interventionCode: iv.intervention_code,
585
+ interventionName: iv.intervention_name,
586
+ applicableDocumentTypes: [attachment.document_type],
587
+ alreadyUploadedTypes: [],
588
+ preselectedDocumentType: attachment.document_type,
589
+ replacementNotice: {
590
+ documentType: attachment.document_type,
591
+ filename: attachment.filename,
592
+ },
593
+ mutate: combinedMutate,
594
+ },
595
+ {},
596
+ {},
597
+ );
598
+ },
599
+ closeModal: () => dispose(),
600
+ });
601
+ },
602
+ [claim.authorization_code, combinedMutate, t],
603
+ );
604
+
605
+ const handleAddDocumentForIntervention = useCallback(
606
+ (iv: PatientClaimIntervention, remainingDocTypes: ReadonlyArray<string>) => {
607
+ launchWorkspace2(
608
+ 'claim-attachments-workspace',
609
+ {
610
+ workspaceTitle: t('addMissingDocument', 'Add missing document'),
611
+ consentToken: claim.authorization_code,
612
+ interventionCode: iv.intervention_code,
613
+ interventionName: iv.intervention_name,
614
+ applicableDocumentTypes: remainingDocTypes,
615
+ alreadyUploadedTypes: [],
616
+ mutate: combinedMutate,
617
+ },
618
+ {},
619
+ {},
620
+ );
621
+ },
622
+ [claim.authorization_code, combinedMutate, t],
623
+ );
624
+
513
625
  return (
514
626
  <div className={styles.expandedContent}>
515
627
  {claim.sync_status === 'ERROR' && claim.sync_error_message && (
@@ -522,19 +634,39 @@ const ClaimDetailsPanel: React.FC<{
522
634
  className={styles.syncErrorNotification}
523
635
  />
524
636
  )}
637
+ {claim.provider_workflow_state === 'DRAFT_RESUBMIT' && (
638
+ <InlineNotification
639
+ kind="info"
640
+ lowContrast
641
+ hideCloseButton
642
+ title={t('claimOpenForChanges', 'Claim is open for changes')}
643
+ subtitle={t('claimOpenForChangesSubtitle', 'You can do edits, and submit back to SHA when ready')}
644
+ className={styles.payerNotice}
645
+ />
646
+ )}
647
+
525
648
  {(claim.payer_workflow_state ||
526
649
  (claim.payer_notes?.length ?? 0) > 0 ||
527
650
  (claim.payer_transitions?.length ?? 0) > 0) && (
528
651
  <section className={styles.payerActivitySection}>
529
652
  {claim.payer_workflow_state === 'SENT_BACK' && (
530
- <InlineNotification
531
- kind="error"
532
- lowContrast
533
- hideCloseButton
534
- title={t('shaBouncedClaim', 'SHA returned this claim for action')}
535
- subtitle={claim.payer_workflow_display_name ?? ''}
536
- className={styles.payerNotice}
537
- />
653
+ <div className={styles.payerNoticeWrap}>
654
+ <InlineNotification
655
+ kind="error"
656
+ lowContrast
657
+ hideCloseButton
658
+ title={t('shaBouncedClaim', 'SHA returned this claim for action')}
659
+ subtitle={claim.payer_workflow_display_name ?? ''}
660
+ className={styles.payerNotice}
661
+ />
662
+ {canMoveToResubmit && (
663
+ <div className={styles.payerNoticeActions}>
664
+ <Button size="sm" kind="danger--tertiary" renderIcon={Renew} onClick={handleMoveToResubmit}>
665
+ {t('openForChanges', 'Open for changes')}
666
+ </Button>
667
+ </div>
668
+ )}
669
+ </div>
538
670
  )}
539
671
 
540
672
  <div className={styles.payerActivityCard}>
@@ -744,6 +876,24 @@ const ClaimDetailsPanel: React.FC<{
744
876
  {t('uploadAttachments', 'Upload attachments')}
745
877
  </Button>
746
878
  )}
879
+ {tab === 'resubmission' &&
880
+ isEditable &&
881
+ (() => {
882
+ const remaining = requiredDocs.filter((d) => !uploadedTypes.has(d));
883
+ if (remaining.length === 0) {
884
+ return null;
885
+ }
886
+ return (
887
+ <Button
888
+ size="sm"
889
+ kind="tertiary"
890
+ renderIcon={DocumentAdd}
891
+ className={styles.addDocBtn}
892
+ onClick={() => handleAddDocumentForIntervention(iv, remaining)}>
893
+ {t('addDocumentN', '+ Add document ({{count}})', { count: remaining.length })}
894
+ </Button>
895
+ );
896
+ })()}
747
897
  </div>
748
898
 
749
899
  {requiredDocs.length > 0 && (
@@ -766,18 +916,36 @@ const ClaimDetailsPanel: React.FC<{
766
916
  </div>
767
917
  {uploadedAttachments.map((a) => (
768
918
  <li key={a.uuid} className={styles.ivAttachmentRow}>
769
- {a.document_type}
770
- {a.uuid ? (
771
- <a
772
- href={`/openmrs${restBaseUrl}/virtualclaims/billing/attachments/${a.uuid}/file`}
773
- target="_blank"
774
- rel="noopener noreferrer"
775
- className={styles.viewLink}>
776
- {t('view', 'View')}
777
- </a>
778
- ) : (
779
- <span className={styles.muted}>—</span>
780
- )}
919
+ <span>{a.document_type}</span>
920
+ <div className={styles.attachmentActions}>
921
+ {a.uuid ? (
922
+ <a
923
+ href={`/openmrs${restBaseUrl}/virtualclaims/billing/attachments/${a.uuid}/file`}
924
+ target="_blank"
925
+ rel="noopener noreferrer"
926
+ className={styles.viewLink}>
927
+ {t('view', 'View')} ↗
928
+ </a>
929
+ ) : (
930
+ <span className={styles.muted}>—</span>
931
+ )}
932
+ {tab === 'resubmission' && isEditable && a.uuid && (
933
+ <Button
934
+ size="sm"
935
+ kind="ghost"
936
+ renderIcon={Renew}
937
+ className={styles.replaceBtn}
938
+ onClick={() =>
939
+ handleReplaceAttachment(iv, {
940
+ uuid: a.uuid,
941
+ document_type: a.document_type ?? '',
942
+ filename: (a as { filename?: string }).filename,
943
+ })
944
+ }>
945
+ {t('replace', 'Replace')}
946
+ </Button>
947
+ )}
948
+ </div>
781
949
  </li>
782
950
  ))}
783
951
  </ul>
package/src/index.ts CHANGED
@@ -259,6 +259,16 @@ export const claimDeleteLineModal = getAsyncLifecycle(
259
259
  options,
260
260
  );
261
261
 
262
+ export const replaceAttachmentModal = getAsyncLifecycle(
263
+ () => import('./claims/claims-wrap/claim-workspaces/attachements/replace-attachment.modal'),
264
+ options,
265
+ );
266
+
267
+ export const resubmitConfirmModal = getAsyncLifecycle(
268
+ () => import('./claims/claims-wrap/claim-workspaces/claim-submission/resubmit-confirm.modal'),
269
+ options,
270
+ );
271
+
262
272
  // Print Preview Components
263
273
  export const printPreviewModal = getAsyncLifecycle(() => import('./print-preview/print-preview.modal'), options);
264
274
  export const patientBannerShaStatus = getAsyncLifecycle(
package/src/routes.json CHANGED
@@ -392,6 +392,14 @@
392
392
  {
393
393
  "name": "require-billing-modal",
394
394
  "component": "requirePaymentModal"
395
+ },
396
+ {
397
+ "name": "replace-claim-attachment-modal",
398
+ "component": "replaceAttachmentModal"
399
+ },
400
+ {
401
+ "name": "resubmit-confirm-modal",
402
+ "component": "resubmitConfirmModal"
395
403
  }
396
404
  ],
397
405
  "workspaces2": [