@tellescope/react-components 1.234.1 → 1.235.1

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 (46) hide show
  1. package/lib/cjs/CMS/ContentViewer.d.ts.map +1 -1
  2. package/lib/cjs/CMS/ContentViewer.js +26 -22
  3. package/lib/cjs/CMS/ContentViewer.js.map +1 -1
  4. package/lib/cjs/Forms/forms.d.ts.map +1 -1
  5. package/lib/cjs/Forms/forms.js +37 -35
  6. package/lib/cjs/Forms/forms.js.map +1 -1
  7. package/lib/cjs/Forms/forms.v2.d.ts.map +1 -1
  8. package/lib/cjs/Forms/forms.v2.js +37 -35
  9. package/lib/cjs/Forms/forms.v2.js.map +1 -1
  10. package/lib/cjs/Forms/inputs.d.ts +17 -2
  11. package/lib/cjs/Forms/inputs.d.ts.map +1 -1
  12. package/lib/cjs/Forms/inputs.js +429 -37
  13. package/lib/cjs/Forms/inputs.js.map +1 -1
  14. package/lib/cjs/Forms/inputs.v2.d.ts +3 -2
  15. package/lib/cjs/Forms/inputs.v2.d.ts.map +1 -1
  16. package/lib/cjs/Forms/inputs.v2.js +21 -294
  17. package/lib/cjs/Forms/inputs.v2.js.map +1 -1
  18. package/lib/cjs/Forms/types.d.ts +4 -0
  19. package/lib/cjs/Forms/types.d.ts.map +1 -1
  20. package/lib/esm/CMS/ContentViewer.d.ts.map +1 -1
  21. package/lib/esm/CMS/ContentViewer.js +27 -23
  22. package/lib/esm/CMS/ContentViewer.js.map +1 -1
  23. package/lib/esm/Forms/forms.d.ts.map +1 -1
  24. package/lib/esm/Forms/forms.js +38 -36
  25. package/lib/esm/Forms/forms.js.map +1 -1
  26. package/lib/esm/Forms/forms.v2.d.ts.map +1 -1
  27. package/lib/esm/Forms/forms.v2.js +38 -36
  28. package/lib/esm/Forms/forms.v2.js.map +1 -1
  29. package/lib/esm/Forms/inputs.d.ts +17 -2
  30. package/lib/esm/Forms/inputs.d.ts.map +1 -1
  31. package/lib/esm/Forms/inputs.js +427 -38
  32. package/lib/esm/Forms/inputs.js.map +1 -1
  33. package/lib/esm/Forms/inputs.v2.d.ts +3 -2
  34. package/lib/esm/Forms/inputs.v2.d.ts.map +1 -1
  35. package/lib/esm/Forms/inputs.v2.js +16 -290
  36. package/lib/esm/Forms/inputs.v2.js.map +1 -1
  37. package/lib/esm/Forms/types.d.ts +4 -0
  38. package/lib/esm/Forms/types.d.ts.map +1 -1
  39. package/lib/tsconfig.tsbuildinfo +1 -1
  40. package/package.json +44 -44
  41. package/src/CMS/ContentViewer.tsx +16 -2
  42. package/src/Forms/forms.tsx +13 -6
  43. package/src/Forms/forms.v2.tsx +9 -2
  44. package/src/Forms/inputs.tsx +563 -66
  45. package/src/Forms/inputs.v2.tsx +13 -594
  46. package/src/Forms/types.ts +4 -2
@@ -1,9 +1,9 @@
1
1
  import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react"
2
2
  import axios from "axios"
3
- import { Autocomplete, Box, Button, Checkbox, Chip, CircularProgress, Collapse, Divider, FormControl, FormControlLabel, FormLabel, Grid, IconButton as MuiIconButton, InputLabel, MenuItem, Radio, RadioGroup, Select, SxProps, TextField, TextFieldProps, Typography } from "@mui/material"
3
+ import { Autocomplete, Box, Button, Checkbox, Chip, CircularProgress, Collapse, Divider, FormControl, FormControlLabel, FormLabel, Grid, IconButton as MuiIconButton, InputLabel, MenuItem, Paper, Radio, RadioGroup, Select, SxProps, TextField, TextFieldProps, Typography } from "@mui/material"
4
4
  import { FormInputProps } from "./types"
5
5
  import { useDropzone } from "react-dropzone"
6
- import { CANVAS_TITLE, EMOTII_TITLE, INSURANCE_RELATIONSHIPS, INSURANCE_RELATIONSHIPS_CANVAS, PRIMARY_HEX, RELATIONSHIP_TYPES, TELLESCOPE_GENDERS } from "@tellescope/constants"
6
+ import { CANVAS_TITLE, BRIDGE_TITLE, EMOTII_TITLE, INSURANCE_RELATIONSHIPS, INSURANCE_RELATIONSHIPS_CANVAS, PRIMARY_HEX, RELATIONSHIP_TYPES, TELLESCOPE_GENDERS } from "@tellescope/constants"
7
7
  import { MM_DD_YYYY_to_YYYY_MM_DD, capture_is_supported, downloadFile, emit_gtm_event, first_letter_capitalized, form_response_value_to_string, format_stripe_subscription_interval, getLocalTimezone, getPublicFileURL, mm_dd_yyyy, object_is_empty, replace_enduser_template_values, responses_satisfy_conditions, truncate_string, update_local_storage, user_display_name } from "@tellescope/utilities"
8
8
  import { Address, DatabaseSelectResponse, Enduser, EnduserRelationship, FormResponseValue, InsuranceRelationship, MedicationResponse, MultipleChoiceOptions, FormFieldOptionDetails, TellescopeGender, TIMEZONES_USA } from "@tellescope/types-models"
9
9
  import { VALID_STATES, emailValidator, phoneValidator } from "@tellescope/validation"
@@ -26,6 +26,16 @@ import { loadStripe } from '@stripe/stripe-js';
26
26
  import { CheckCircleOutline, Delete, Edit, ExpandMore } from "@mui/icons-material"
27
27
  import { WYSIWYG } from "./wysiwyg"
28
28
 
29
+ // Bridge Eligibility - shared variable for storing most recent eligibility userIds
30
+ const bridgeEligibilityResult = {
31
+ userIds: [] as string[],
32
+ }
33
+
34
+ export const getBridgeEligibilityUserIds = () => bridgeEligibilityResult.userIds
35
+ export const setBridgeEligibilityUserIds = (userIds: string[]) => {
36
+ bridgeEligibilityResult.userIds = userIds
37
+ }
38
+
29
39
  // Debounce hook for search functionality
30
40
  const useDebounce = <T,>(value: T, delay: number): T => {
31
41
  const [debouncedValue, setDebouncedValue] = useState<T>(value)
@@ -409,15 +419,19 @@ export const TableInput = ({ field, value=[], onChange, ...props }: FormInputPro
409
419
  )
410
420
  }
411
421
 
412
- export const AutoFocusTextField = (props: TextFieldProps) => (
413
- <TextField InputProps={defaultInputProps} {...props} />
414
- )
422
+ export const AutoFocusTextField = (props: TextFieldProps & { inputProps?: { sx: SxProps } }) => {
423
+ const { inputProps, ...textFieldProps } = props
424
+ return <TextField InputProps={inputProps || defaultInputProps} {...textFieldProps} />
425
+ }
415
426
 
416
- const CustomDateStringInput = forwardRef((props: TextFieldProps, ref) => (
417
- <TextField InputProps={defaultInputProps}
418
- fullWidth inputRef={ref} {...props}
419
- />
420
- ))
427
+ const CustomDateStringInput = forwardRef((props: TextFieldProps & { inputProps?: { sx: SxProps } }, ref) => {
428
+ const { inputProps, ...textFieldProps } = props
429
+ return (
430
+ <TextField InputProps={inputProps || defaultInputProps}
431
+ fullWidth inputRef={ref} {...textFieldProps}
432
+ />
433
+ )
434
+ })
421
435
  export const DateStringInput = ({ field, value, onChange, ...props }: FormInputProps<'string'>) => {
422
436
  const inputRef = useRef(null);
423
437
 
@@ -543,7 +557,9 @@ export const NumberInput = ({ field, value, onChange, form, ...props }: FormInpu
543
557
  )
544
558
  }
545
559
 
546
- export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form, responses, enduser, ...props }: FormInputProps<'Insurance'>) => {
560
+ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form, responses, enduser, inputProps, ...props }: FormInputProps<'Insurance'> & {
561
+ inputProps?: { sx: SxProps },
562
+ }) => {
547
563
  const session = useResolvedSession()
548
564
 
549
565
  const [payers, setPayers] = useState<{ id: string, name: string, databaseRecord?: DatabaseRecord, type?: string, state?: string }[]>([])
@@ -563,9 +579,11 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
563
579
  (addressQuestion?.answer?.type === 'Address' ? addressQuestion?.answer?.value?.state : undefined) || enduser?.state
564
580
  ), [enduser?.state, addressQuestion])
565
581
 
582
+ // load from database
566
583
  const loadRef = useRef(false) // so session changes don't cause
567
584
  useEffect(() => {
568
585
  if (field?.options?.dataSource === CANVAS_TITLE) return // instead, look-up while typing against Canvas Search API
586
+ if (field?.options?.dataSource === BRIDGE_TITLE) return // instead, look-up while typing against Bridge Search API
569
587
  if (loadRef.current) return
570
588
  loadRef.current = true
571
589
 
@@ -585,27 +603,35 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
585
603
  .catch(console.error)
586
604
  }, [session, state, field?.options?.dataSource])
587
605
 
606
+ // load from 3rd-party on search only
588
607
  const searchRef = useRef(query)
589
608
  useEffect(() => {
590
- if (field?.options?.dataSource !== CANVAS_TITLE) { return }
609
+ if (field?.options?.dataSource !== CANVAS_TITLE && field?.options?.dataSource !== BRIDGE_TITLE) { return }
591
610
  if (!query) return
592
611
  if (searchRef.current === query) return
593
612
  searchRef.current = query
594
613
 
595
- session.api.integrations.proxy_read({
596
- integration: CANVAS_TITLE,
597
- query,
598
- type: 'organizations',
599
- })
600
- .then(({ data }) => {
601
- try {
602
- setPayers(data.map((d: any) => ({
603
- id: d.resource.id,
604
- name: d.resource.name,
605
- })))
606
- } catch(err) { console.error }
607
- })
608
- .catch(console.error)
614
+ const integration = field?.options?.dataSource === CANVAS_TITLE ? CANVAS_TITLE : BRIDGE_TITLE
615
+ const type = field?.options?.dataSource === CANVAS_TITLE ? 'organizations' : 'payers'
616
+
617
+ const t = setTimeout(() => (
618
+ session.api.integrations.proxy_read({
619
+ integration,
620
+ query,
621
+ type,
622
+ })
623
+ .then(({ data }) => {
624
+ try {
625
+ setPayers(data.map((d: any) => ({
626
+ id: field?.options?.dataSource === CANVAS_TITLE ? d.resource.id : d.id,
627
+ name: field?.options?.dataSource === CANVAS_TITLE ? d.resource.name : d.name,
628
+ })))
629
+ } catch(err) { console.error }
630
+ })
631
+ .catch(console.error)
632
+ ), 300)
633
+
634
+ return () => { clearTimeout(t) }
609
635
  }, [session, field?.options?.dataSource, query])
610
636
 
611
637
  return (
@@ -630,24 +656,25 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
630
656
  onDatabaseSelect?.([databaseRecord])
631
657
  }
632
658
 
659
+ // don't lose existing payerId on back-and-forth navigation
633
660
  onChange({
634
661
  ...value,
635
662
  payerName: v || '',
636
- payerId: payers.find(p => p.name === v)?.id || '',
663
+ payerId: (value?.payerName === v && value?.payerId ? value.payerId : '') || payers.find(p => p.name === v)?.id || '',
637
664
  payerType: payers.find(p => p.name === v)?.type || '',
638
665
  }, field.id)
639
666
  }
640
667
  }
641
668
  renderInput={(params) => (
642
- <TextField {...params} InputProps={{ ...params.InputProps, sx: defaultInputProps.sx }}
669
+ <TextField {...params} InputProps={{ ...params.InputProps, sx: (inputProps || defaultInputProps).sx }}
643
670
  required={!field.isOptional} size="small" label={"Insurer"}
644
- placeholder={field.options?.dataSource === CANVAS_TITLE ? "Search insurer..." : "Insurer"}
671
+ placeholder={(field.options?.dataSource === CANVAS_TITLE || field.options?.dataSource === BRIDGE_TITLE) ? "Search insurer..." : "Insurer"}
645
672
  />
646
673
  )}
647
674
  />
648
675
  </Grid>
649
676
  <Grid item xs={12} sm={6}>
650
- <TextField InputProps={defaultInputProps} required={!field.isOptional} fullWidth value={value?.memberId ?? ''}
677
+ <TextField InputProps={inputProps || defaultInputProps} required={!field.isOptional} fullWidth value={value?.memberId ?? ''}
651
678
  onChange={e => onChange({ ...value, memberId: e.target.value }, field.id)}
652
679
  label={form_display_text_for_language(form, "Member ID", '')}
653
680
  size="small"
@@ -655,8 +682,8 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
655
682
  </Grid>
656
683
 
657
684
  <Grid item xs={12} sm={6}>
658
- <TextField InputProps={defaultInputProps} required={false} fullWidth value={value?.planName ?? ''}
659
- onChange={e => onChange({ ...value, planName: e.target.value }, field.id)}
685
+ <TextField InputProps={inputProps || defaultInputProps} required={false} fullWidth value={value?.planName ?? ''}
686
+ onChange={e => onChange({ ...value, planName: e.target.value }, field.id)}
660
687
  label={form_display_text_for_language(form, "Plan Name", '')}
661
688
  size="small"
662
689
  />
@@ -664,14 +691,15 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
664
691
 
665
692
  <Grid item xs={12} sm={6}>
666
693
  <DateStringInput size="small" label="Plan Start Date"
694
+ inputProps={inputProps}
667
695
  field={{
668
696
  ...field,
669
697
  isOptional: true, //field.isOptional || field.options?.billingProvider === 'Candid'
670
- }}
671
- value={value?.startDate || ''}
672
- onChange={startDate =>
673
- onChange({
674
- ...value,
698
+ }}
699
+ value={value?.startDate || ''}
700
+ onChange={startDate =>
701
+ onChange({
702
+ ...value,
675
703
  startDate,
676
704
  }, field.id)
677
705
  }
@@ -680,7 +708,7 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
680
708
 
681
709
  {field.options?.includeGroupNumber &&
682
710
  <Grid item xs={12}>
683
- <TextField InputProps={defaultInputProps} fullWidth value={value?.groupNumber ?? ''}
711
+ <TextField InputProps={inputProps || defaultInputProps} fullWidth value={value?.groupNumber ?? ''}
684
712
  onChange={e => onChange({ ...value, groupNumber: e.target.value }, field.id)}
685
713
  label={form_display_text_for_language(form, "Group Number", '')}
686
714
  size="small"
@@ -690,16 +718,17 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
690
718
 
691
719
  <Grid item xs={12}>
692
720
  <StringSelector size="small" label="Relationship to Policy Owner"
721
+ inputProps={inputProps}
693
722
  options={
694
723
  (
695
724
  (field.options?.billingProvider === CANVAS_TITLE || field.options?.dataSource === CANVAS_TITLE )
696
- ? INSURANCE_RELATIONSHIPS_CANVAS
725
+ ? INSURANCE_RELATIONSHIPS_CANVAS
697
726
  : INSURANCE_RELATIONSHIPS
698
727
  )
699
728
  .sort((x, y) => x.localeCompare(y))
700
729
  }
701
- value={value?.relationship || 'Self'}
702
- onChange={relationship =>
730
+ value={value?.relationship || 'Self'}
731
+ onChange={relationship =>
703
732
  onChange({ ...value, relationship: relationship as InsuranceRelationship || 'Self' }, field.id)
704
733
  }
705
734
  />
@@ -712,24 +741,24 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
712
741
  </Grid>
713
742
 
714
743
  <Grid item xs={6}>
715
- <TextField label="First Name" size="small" InputProps={defaultInputProps} fullWidth
716
- value={value?.relationshipDetails?.fname || ''}
744
+ <TextField label="First Name" size="small" InputProps={inputProps || defaultInputProps} fullWidth
745
+ value={value?.relationshipDetails?.fname || ''}
717
746
  required={!field.isOptional}
718
- onChange={e =>
719
- onChange({
720
- ...value,
747
+ onChange={e =>
748
+ onChange({
749
+ ...value,
721
750
  relationshipDetails: { ...value?.relationshipDetails, fname: e.target.value }
722
751
  }, field.id)
723
752
  }
724
753
  />
725
754
  </Grid>
726
755
  <Grid item xs={6}>
727
- <TextField label="Last Name" size="small" InputProps={defaultInputProps} fullWidth
728
- value={value?.relationshipDetails?.lname || ''}
756
+ <TextField label="Last Name" size="small" InputProps={inputProps || defaultInputProps} fullWidth
757
+ value={value?.relationshipDetails?.lname || ''}
729
758
  required={!field.isOptional}
730
- onChange={e =>
731
- onChange({
732
- ...value,
759
+ onChange={e =>
760
+ onChange({
761
+ ...value,
733
762
  relationshipDetails: { ...value?.relationshipDetails, lname: e.target.value }
734
763
  }, field.id)
735
764
  }
@@ -737,11 +766,12 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
737
766
  </Grid>
738
767
  <Grid item xs={6}>
739
768
  <StringSelector options={TELLESCOPE_GENDERS} size="small" label="Gender"
740
- value={value?.relationshipDetails?.gender || ''}
769
+ inputProps={inputProps}
770
+ value={value?.relationshipDetails?.gender || ''}
741
771
  required={!field.isOptional}
742
- onChange={v =>
743
- onChange({
744
- ...value,
772
+ onChange={v =>
773
+ onChange({
774
+ ...value,
745
775
  relationshipDetails: { ...value?.relationshipDetails, gender: v as TellescopeGender }
746
776
  }, field.id)
747
777
  }
@@ -749,14 +779,15 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
749
779
  </Grid>
750
780
  <Grid item xs={6}>
751
781
  <DateStringInput size="small" label="Date of Birth"
782
+ inputProps={inputProps}
752
783
  field={{
753
784
  ...field,
754
785
  isOptional: field.isOptional || field.options?.billingProvider === 'Candid'
755
- }}
756
- value={value?.relationshipDetails?.dateOfBirth || ''}
757
- onChange={dateOfBirth =>
758
- onChange({
759
- ...value,
786
+ }}
787
+ value={value?.relationshipDetails?.dateOfBirth || ''}
788
+ onChange={dateOfBirth =>
789
+ onChange({
790
+ ...value,
760
791
  relationshipDetails: { ...value?.relationshipDetails, dateOfBirth }
761
792
  }, field.id)
762
793
  }
@@ -894,8 +925,8 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
894
925
  field.id
895
926
  )}
896
927
  renderInput={(params) => (
897
- <TextField {...params} InputProps={{ ...params.InputProps, sx: defaultInputProps.sx }}
898
- size={'small'} label={"State"} required={!field.isOptional}
928
+ <TextField {...params} InputProps={{ ...params.InputProps, sx: (inputProps || defaultInputProps).sx }}
929
+ size={'small'} label={"State"} required={!field.isOptional}
899
930
  />
900
931
  )}
901
932
  {...props}
@@ -908,7 +939,7 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
908
939
  }
909
940
 
910
941
 
911
- const StringSelector = ({ options, value, onChange, required, getDisplayValue, ...props } : {
942
+ const StringSelector = ({ options, value, onChange, required, getDisplayValue, inputProps, ...props } : {
912
943
  options: string[]
913
944
  value: string,
914
945
  onChange: (v: string) => void,
@@ -917,11 +948,12 @@ const StringSelector = ({ options, value, onChange, required, getDisplayValue, .
917
948
  required?: boolean,
918
949
  getDisplayValue?: (v: string) => string,
919
950
  disabled?: boolean,
951
+ inputProps?: { sx: SxProps },
920
952
  }) => (
921
953
  <FormControl fullWidth size={props.size} required={required}>
922
954
  <InputLabel>{props.label}</InputLabel>
923
955
  <Select {...props} value={value} onChange={e => onChange(e.target.value)} fullWidth
924
- sx={defaultInputProps.sx}
956
+ sx={(inputProps || defaultInputProps).sx}
925
957
  >
926
958
  {options.map((o, i) => (
927
959
  <MenuItem value={o} key={o || i}>{getDisplayValue?.(o) ?? o}</MenuItem>
@@ -930,9 +962,464 @@ const StringSelector = ({ options, value, onChange, required, getDisplayValue, .
930
962
  </FormControl>
931
963
  )
932
964
 
965
+ export const BridgeEligibilityInput = ({ field, value, onChange, responses, enduser, inputProps, enduserId, ...props }: FormInputProps<'Bridge Eligibility'> & {
966
+ inputProps?: { sx: SxProps },
967
+ }) => {
968
+ const session = useResolvedSession()
969
+ const [loading, setLoading] = useState(false)
970
+ const [polling, setPolling] = useState(false)
971
+ const [error, setError] = useState<string>()
972
+
973
+ // single-page form must require button-click to check, but 1-page-at-a-time enduser sessions should auto-check
974
+ const isEnduserSession = session.type === 'enduser'
975
+ const eligibilityType = field.options?.bridgeEligibilityType || 'Soft'
976
+
977
+ // Extract payerId from Insurance question response
978
+ const [payerId, memberId, payerName] = useMemo(() => {
979
+ const insuranceResponse = responses?.find(r => r.answer?.type === 'Insurance' && r.answer?.value?.payerId)
980
+ if (insuranceResponse?.answer?.type === 'Insurance') {
981
+ return [
982
+ insuranceResponse.answer.value?.payerId,
983
+ insuranceResponse.answer.value?.memberId,
984
+ insuranceResponse.answer.value?.payerName,
985
+ ]
986
+ }
987
+ // existing payer id is automatically resolved on the backend as default
988
+ return []
989
+ }, [responses])
990
+
991
+ // Extract state from Address question or enduser
992
+ const state = useMemo(() => {
993
+ // Find Address field with state value
994
+ const addressResponse = responses?.find(r =>
995
+ r.answer?.type === 'Address' && r.answer?.value?.state
996
+ )
997
+ if (addressResponse?.answer?.type === 'Address') {
998
+ return addressResponse.answer.value?.state
999
+ }
1000
+ // enduser state is automatically resolved on the backend as default
1001
+ }, [responses])
1002
+
1003
+ // Soft eligibility check function - supports multiple service type IDs
1004
+ const checkProviderEligibility = useCallback(async () => {
1005
+ const serviceTypeIds = field.options?.bridgeServiceTypeIds
1006
+
1007
+ if (!serviceTypeIds || serviceTypeIds.length === 0) {
1008
+ setError('Bridge Service Type IDs not configured')
1009
+ return
1010
+ }
1011
+ // payerId and state can be automatically resolved on the backend, if already saved on Enduser, so not required here
1012
+
1013
+ setLoading(true)
1014
+ setError(undefined)
1015
+
1016
+ try {
1017
+ // Fire parallel requests for each service type ID
1018
+ const results = await Promise.all(
1019
+ serviceTypeIds.map(async (serviceTypeId) => {
1020
+ try {
1021
+ const { data } = await session.api.integrations.proxy_read({
1022
+ id: enduserId,
1023
+ integration: BRIDGE_TITLE,
1024
+ type: 'provider-eligibility',
1025
+ query: JSON.stringify({
1026
+ serviceTypeId,
1027
+ payerId,
1028
+ state,
1029
+ }),
1030
+ })
1031
+ return {
1032
+ serviceTypeId,
1033
+ status: data?.status || 'unknown',
1034
+ userIds: data?.userIds || [],
1035
+ }
1036
+ } catch (err: any) {
1037
+ console.error(`Provider eligibility check failed for ${serviceTypeId}:`, err)
1038
+ return {
1039
+ serviceTypeId,
1040
+ status: 'error',
1041
+ userIds: [],
1042
+ error: err?.message,
1043
+ }
1044
+ }
1045
+ })
1046
+ )
1047
+
1048
+ // Aggregate results - union of userIds across all service types
1049
+ const allUserIds = results.flatMap(r => r.userIds)
1050
+ const uniqueUserIds = Array.from(new Set(allUserIds))
1051
+
1052
+ // Determine aggregated status (ELIGIBLE if any are eligible, otherwise first non-error status)
1053
+ const aggregatedStatus = results.some(r => r.status === 'ELIGIBLE')
1054
+ ? 'ELIGIBLE'
1055
+ : results.find(r => r.status !== 'error')?.status || 'unknown'
1056
+
1057
+ // Store aggregated userIds in shared variable for Appointment Booking to use
1058
+ setBridgeEligibilityUserIds(uniqueUserIds)
1059
+
1060
+ // Update the answer with aggregated results
1061
+ onChange({
1062
+ payerId, // Store payerId to detect changes on remount
1063
+ status: aggregatedStatus,
1064
+ userIds: uniqueUserIds,
1065
+ }, field.id)
1066
+ } catch (err: any) {
1067
+ setError(err?.message || 'Failed to check eligibility')
1068
+ console.error('Provider eligibility check failed:', err)
1069
+ } finally {
1070
+ setLoading(false)
1071
+ }
1072
+ }, [session, field, payerId, state, onChange, enduserId])
1073
+
1074
+ // Hard eligibility check function with polling - supports multiple service type IDs
1075
+ const checkServiceEligibility = useCallback(async () => {
1076
+ const serviceTypeIds = field.options?.bridgeServiceTypeIds
1077
+
1078
+ if (!serviceTypeIds || serviceTypeIds.length === 0) {
1079
+ setError('Bridge Service Type IDs not configured')
1080
+ return
1081
+ }
1082
+
1083
+ setLoading(true)
1084
+ setError(undefined)
1085
+
1086
+ try {
1087
+ // Initiate service eligibility checks for all service type IDs in parallel
1088
+ const initiatedChecks = await Promise.all(
1089
+ serviceTypeIds.map(async (serviceTypeId) => {
1090
+ try {
1091
+ const { data } = await session.api.integrations.proxy_read({
1092
+ id: enduserId,
1093
+ integration: BRIDGE_TITLE,
1094
+ type: 'service-eligibility',
1095
+ query: JSON.stringify({
1096
+ serviceTypeId,
1097
+ payerId,
1098
+ memberId,
1099
+ state,
1100
+ }),
1101
+ })
1102
+
1103
+ const serviceEligibilityId = data?.id
1104
+ if (!serviceEligibilityId) {
1105
+ throw new Error('No service eligibility ID returned')
1106
+ }
1107
+
1108
+ return {
1109
+ serviceTypeId,
1110
+ serviceEligibilityId,
1111
+ error: undefined,
1112
+ }
1113
+ } catch (err: any) {
1114
+ console.error(`Service eligibility check initiation failed for ${serviceTypeId}:`, err)
1115
+ return {
1116
+ serviceTypeId,
1117
+ serviceEligibilityId: null,
1118
+ error: err?.message,
1119
+ }
1120
+ }
1121
+ })
1122
+ )
1123
+
1124
+ setLoading(false)
1125
+ setPolling(true)
1126
+
1127
+ // Poll for results from all checks in parallel
1128
+ const pollForAllResults = async () => {
1129
+ const maxAttempts = 60 // Poll for up to 60 attempts (2 minutes at 2s intervals)
1130
+
1131
+ // Track completion status for each check
1132
+ const checkStatuses = new Map(
1133
+ initiatedChecks.map(check => [
1134
+ check.serviceTypeId,
1135
+ {
1136
+ completed: check.error !== undefined || check.serviceEligibilityId === null,
1137
+ result: check.error ? { status: 'error', userIds: [], error: check.error } : null,
1138
+ serviceEligibilityId: check.serviceEligibilityId,
1139
+ }
1140
+ ])
1141
+ )
1142
+
1143
+ let attempts = 0
1144
+
1145
+ const pollAll = async (): Promise<void> => {
1146
+ if (attempts >= maxAttempts) {
1147
+ setError('Eligibility check timed out. Please try again.')
1148
+ setPolling(false)
1149
+ return
1150
+ }
1151
+
1152
+ attempts++
1153
+
1154
+ // Poll all incomplete checks in parallel
1155
+ const pollPromises = initiatedChecks
1156
+ .filter(check => {
1157
+ const status = checkStatuses.get(check.serviceTypeId)
1158
+ return check.serviceEligibilityId && status && !status.completed
1159
+ })
1160
+ .map(async (check) => {
1161
+ try {
1162
+ const { data: pollData } = await session.api.integrations.proxy_read({
1163
+ id: check.serviceEligibilityId!,
1164
+ integration: BRIDGE_TITLE,
1165
+ type: 'service-eligibility-poll',
1166
+ })
1167
+
1168
+ const status = pollData?.status
1169
+
1170
+ // Check if we're in a terminal state
1171
+ if (status && status !== 'PENDING') {
1172
+ const checkStatus = checkStatuses.get(check.serviceTypeId)!
1173
+ checkStatus.completed = true
1174
+ checkStatus.result = {
1175
+ status: status || 'unknown',
1176
+ userIds: pollData?.userIds || [],
1177
+ error: undefined,
1178
+ }
1179
+ }
1180
+ } catch (err: any) {
1181
+ console.error(`Service eligibility polling failed for ${check.serviceTypeId}:`, err)
1182
+ const checkStatus = checkStatuses.get(check.serviceTypeId)!
1183
+ checkStatus.completed = true
1184
+ checkStatus.result = {
1185
+ status: 'error',
1186
+ userIds: [],
1187
+ error: err?.message,
1188
+ }
1189
+ }
1190
+ })
1191
+
1192
+ await Promise.all(pollPromises)
1193
+
1194
+ // Check if all checks are completed
1195
+ const allCompleted = Array.from(checkStatuses.values()).every(s => s.completed)
1196
+
1197
+ if (allCompleted) {
1198
+ // Aggregate results - union of userIds across all service types
1199
+ const results = Array.from(checkStatuses.entries()).map(([serviceTypeId, status]) => ({
1200
+ serviceTypeId,
1201
+ status: status.result?.status || 'unknown',
1202
+ userIds: status.result?.userIds || [],
1203
+ }))
1204
+
1205
+ const allUserIds = results.flatMap(r => r.userIds)
1206
+ const uniqueUserIds = Array.from(new Set(allUserIds))
1207
+
1208
+ // Determine aggregated status (ELIGIBLE if any are eligible, otherwise first non-error status)
1209
+ const aggregatedStatus = results.some(r => r.status === 'ELIGIBLE')
1210
+ ? 'ELIGIBLE'
1211
+ : results.find(r => r.status !== 'error')?.status || 'unknown'
1212
+
1213
+ // Store aggregated userIds in shared variable for Appointment Booking to use
1214
+ setBridgeEligibilityUserIds(uniqueUserIds)
1215
+
1216
+ // Update the answer with aggregated results
1217
+ onChange({
1218
+ payerId, // Store payerId to detect changes on remount
1219
+ status: aggregatedStatus,
1220
+ userIds: uniqueUserIds,
1221
+ }, field.id)
1222
+
1223
+ setPolling(false)
1224
+ return
1225
+ }
1226
+
1227
+ // Still have pending checks, poll again after delay
1228
+ setTimeout(pollAll, 2000) // Poll every 2 seconds
1229
+ }
1230
+
1231
+ pollAll()
1232
+ }
1233
+
1234
+ pollForAllResults()
1235
+ } catch (err: any) {
1236
+ setError(err?.message || 'Failed to check service eligibility')
1237
+ console.error('Service eligibility check failed:', err)
1238
+ setLoading(false)
1239
+ setPolling(false)
1240
+ }
1241
+ }, [session, field, payerId, memberId, state, onChange, enduserId])
1242
+
1243
+ // Auto-check eligibility for enduser sessions
1244
+ const autoCheckRef = useRef(false)
1245
+ useEffect(() => {
1246
+ if (!isEnduserSession) return
1247
+
1248
+ // If we already have a result and the payer hasn't changed, use the cached result
1249
+ if (value?.status && value?.payerId === payerId) {
1250
+ return
1251
+ }
1252
+
1253
+ if (autoCheckRef.current) return
1254
+ autoCheckRef.current = true
1255
+
1256
+ if (eligibilityType === 'Hard') {
1257
+ checkServiceEligibility()
1258
+ } else {
1259
+ checkProviderEligibility()
1260
+ }
1261
+ }, [isEnduserSession, eligibilityType, checkProviderEligibility, checkServiceEligibility, value, payerId])
1262
+
1263
+ const errorComponent = useMemo(() => (
1264
+ <Grid container spacing={2} direction="column" alignItems="center" style={{ padding: '20px 0' }}>
1265
+ <Grid item>
1266
+ <Paper style={{
1267
+ padding: 16,
1268
+ backgroundColor: '#ffebee',
1269
+ border: '2px solid #f44336'
1270
+ }}>
1271
+ <Grid container spacing={2} direction="column" alignItems="center">
1272
+ <Grid item>
1273
+ <Typography variant="h2" style={{ color: '#f44336' }}>⚠️</Typography>
1274
+ </Grid>
1275
+ <Grid item>
1276
+ <Typography variant="h6" align="center" color="error">
1277
+ Unable to Check Eligibility
1278
+ </Typography>
1279
+ </Grid>
1280
+ <Grid item>
1281
+ <Typography variant="body2" align="center" style={{ color: '#d32f2f' }}>
1282
+ {error}
1283
+ </Typography>
1284
+ </Grid>
1285
+ </Grid>
1286
+ </Paper>
1287
+ </Grid>
1288
+ </Grid>
1289
+ ), [error])
1290
+
1291
+ const checkingEligibilityComponent = useMemo(() => (
1292
+ <Grid container spacing={2} direction="column" alignItems="center" style={{ padding: '20px 0' }}>
1293
+ <Grid item>
1294
+ <CircularProgress size={40} />
1295
+ </Grid>
1296
+ <Grid item>
1297
+ <Typography variant="body1">
1298
+ {polling ? 'Verifying eligibility with insurance...' : 'Checking eligibility...'}
1299
+ </Typography>
1300
+ </Grid>
1301
+ <Grid item>
1302
+ <Typography variant="body2" color="textSecondary">
1303
+ {polling ? 'This usually takes 15-30 seconds' : 'This may take a few moments'}
1304
+ </Typography>
1305
+ </Grid>
1306
+ </Grid>
1307
+ ), [polling])
1308
+
1309
+ const resultsComponent = useMemo(() => {
1310
+ const isEligible = value?.status === 'ELIGIBLE'
1311
+ return (
1312
+ <Grid container spacing={2} direction="column">
1313
+ <Grid item>
1314
+ <Paper style={{
1315
+ padding: 16,
1316
+ backgroundColor: isEligible ? '#e8f5e9' : '#fff3e0',
1317
+ border: `2px solid ${isEligible ? '#4caf50' : '#ff9800'}`
1318
+ }}>
1319
+ <Grid container spacing={2} direction="column" alignItems="center">
1320
+ <Grid item>
1321
+ {isEligible ? (
1322
+ <CheckCircleOutline style={{ fontSize: 48, color: '#4caf50' }} />
1323
+ ) : (
1324
+ <Typography variant="h2" style={{ color: '#ff9800' }}>⚠️</Typography>
1325
+ )}
1326
+ </Grid>
1327
+ <Grid item>
1328
+ <Typography variant="h6" align="center">
1329
+ {isEligible
1330
+ ? `${payerName || 'Your insurance provider'} is accepted!`
1331
+ : 'Eligibility Status: ' + first_letter_capitalized((value?.status || 'Unknown').toLowerCase())
1332
+ }
1333
+ </Typography>
1334
+ </Grid>
1335
+ </Grid>
1336
+ </Paper>
1337
+ </Grid>
1338
+ </Grid>
1339
+ )
1340
+ }, [value, payerName])
1341
+
1342
+ // Loading/polling state for enduser sessions
1343
+ if (isEnduserSession) {
1344
+ if (loading || polling) { return checkingEligibilityComponent }
1345
+ if (error) {
1346
+ return errorComponent
1347
+ }
1348
+ if (value?.status) {
1349
+ return resultsComponent
1350
+ }
1351
+ return errorComponent
1352
+ }
1353
+
1354
+ // User/admin interface (non-enduser sessions)
1355
+ return (
1356
+ <Grid container spacing={2} direction="column">
1357
+ <Grid item>
1358
+ <Typography variant="body2" color="textSecondary">
1359
+ Eligibility Type: {eligibilityType}
1360
+ </Typography>
1361
+ <Typography variant="body2" color="textSecondary">
1362
+ Service Type IDs: {field.options?.bridgeServiceTypeIds?.join(', ') || 'Not configured'}
1363
+ </Typography>
1364
+ {state && <Typography variant="body2" color="textSecondary">State: {state}</Typography>}
1365
+ {payerId && <Typography variant="body2" color="textSecondary">Payer ID: {payerId}</Typography>}
1366
+ {memberId && <Typography variant="body2" color="textSecondary">Member ID: {memberId}</Typography>}
1367
+ </Grid>
1368
+
1369
+ {error && (
1370
+ <Grid item>
1371
+ <Typography variant="body2" color="error">{error}</Typography>
1372
+ </Grid>
1373
+ )}
1374
+
1375
+ {polling && (
1376
+ <Grid item>
1377
+ <Typography variant="body2" color="primary">
1378
+ Polling for results... (this may take 15-30 seconds)
1379
+ </Typography>
1380
+ </Grid>
1381
+ )}
1382
+
1383
+ <Grid item container spacing={2}>
1384
+ <Grid item>
1385
+ <LoadingButton
1386
+ variant="outlined"
1387
+ onClick={checkProviderEligibility}
1388
+ submitText="Check Provider Eligibility (Free)"
1389
+ submittingText="Checking..."
1390
+ submitting={loading && !polling}
1391
+ disabled={!field.options?.bridgeServiceTypeIds?.length || loading || polling}
1392
+ />
1393
+ </Grid>
1394
+ <Grid item>
1395
+ <LoadingButton
1396
+ variant="outlined"
1397
+ onClick={checkServiceEligibility}
1398
+ submitText="Check Service Eligibility (Paid)"
1399
+ submittingText={polling ? "Polling..." : "Initiating..."}
1400
+ submitting={loading || polling}
1401
+ disabled={!field.options?.bridgeServiceTypeIds?.length || loading || polling}
1402
+ />
1403
+ </Grid>
1404
+ </Grid>
1405
+
1406
+ {value && (
1407
+ <Grid item>
1408
+ <Typography variant="caption" color="textSecondary">
1409
+ Current Answer:
1410
+ </Typography>
1411
+ <pre style={{ fontSize: 11, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
1412
+ {JSON.stringify(value, null, 2)}
1413
+ </pre>
1414
+ </Grid>
1415
+ )}
1416
+ </Grid>
1417
+ )
1418
+ }
1419
+
933
1420
  const HourSelector = (props : { value: string, onChange: (v: string) => void }) => (
934
- <StringSelector {...props}
935
- options={Array(12).fill('').map((_, i) => (i + 1) <= 9 ? `0${i + 1}` : (i + 1).toString())}
1421
+ <StringSelector {...props}
1422
+ options={Array(12).fill('').map((_, i) => (i + 1) <= 9 ? `0${i + 1}` : (i + 1).toString())}
936
1423
  />
937
1424
  )
938
1425
  const MinuteSelector = (props : { value: string, onChange: (v: string) => void }) => (
@@ -3685,6 +4172,16 @@ export const AppointmentBookingInput = ({ formResponseId, field, value, onChange
3685
4172
  .join(',')
3686
4173
  }`
3687
4174
  }
4175
+ // Filter to Bridge eligibility userIds if option is enabled
4176
+ if (field.options?.useBridgeEligibilityResult) {
4177
+ const bridgeUserIds = getBridgeEligibilityUserIds()
4178
+
4179
+ if (bridgeUserIds.length === 0) {
4180
+ return <Typography>No eligible users found for booking</Typography>
4181
+ }
4182
+
4183
+ bookingURL += `&userIds=${bridgeUserIds.join(',')}`
4184
+ }
3688
4185
  // need to use form?.id for internally-submitted forms because formResponseId isn't generated until initial submission or saved draft
3689
4186
  if (field.options?.holdAppointmentMinutes && (formResponseId || field?.id)) {
3690
4187
  bookingURL += `&formResponseId=${formResponseId || field?.id}`