@tellescope/react-components 1.234.1 → 1.235.0

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 (39) hide show
  1. package/lib/cjs/Forms/forms.d.ts.map +1 -1
  2. package/lib/cjs/Forms/forms.js +37 -35
  3. package/lib/cjs/Forms/forms.js.map +1 -1
  4. package/lib/cjs/Forms/forms.v2.d.ts.map +1 -1
  5. package/lib/cjs/Forms/forms.v2.js +37 -35
  6. package/lib/cjs/Forms/forms.v2.js.map +1 -1
  7. package/lib/cjs/Forms/inputs.d.ts +17 -2
  8. package/lib/cjs/Forms/inputs.d.ts.map +1 -1
  9. package/lib/cjs/Forms/inputs.js +308 -36
  10. package/lib/cjs/Forms/inputs.js.map +1 -1
  11. package/lib/cjs/Forms/inputs.v2.d.ts +3 -2
  12. package/lib/cjs/Forms/inputs.v2.d.ts.map +1 -1
  13. package/lib/cjs/Forms/inputs.v2.js +20 -293
  14. package/lib/cjs/Forms/inputs.v2.js.map +1 -1
  15. package/lib/cjs/Forms/types.d.ts +4 -0
  16. package/lib/cjs/Forms/types.d.ts.map +1 -1
  17. package/lib/esm/Forms/forms.d.ts.map +1 -1
  18. package/lib/esm/Forms/forms.js +38 -36
  19. package/lib/esm/Forms/forms.js.map +1 -1
  20. package/lib/esm/Forms/forms.v2.d.ts.map +1 -1
  21. package/lib/esm/Forms/forms.v2.js +38 -36
  22. package/lib/esm/Forms/forms.v2.js.map +1 -1
  23. package/lib/esm/Forms/inputs.d.ts +17 -2
  24. package/lib/esm/Forms/inputs.d.ts.map +1 -1
  25. package/lib/esm/Forms/inputs.js +306 -37
  26. package/lib/esm/Forms/inputs.js.map +1 -1
  27. package/lib/esm/Forms/inputs.v2.d.ts +3 -2
  28. package/lib/esm/Forms/inputs.v2.d.ts.map +1 -1
  29. package/lib/esm/Forms/inputs.v2.js +15 -289
  30. package/lib/esm/Forms/inputs.v2.js.map +1 -1
  31. package/lib/esm/Forms/types.d.ts +4 -0
  32. package/lib/esm/Forms/types.d.ts.map +1 -1
  33. package/lib/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +9 -9
  35. package/src/Forms/forms.tsx +13 -6
  36. package/src/Forms/forms.v2.tsx +9 -2
  37. package/src/Forms/inputs.tsx +449 -65
  38. package/src/Forms/inputs.v2.tsx +12 -593
  39. 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 }[]>([])
@@ -566,6 +582,7 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
566
582
  const loadRef = useRef(false) // so session changes don't cause
567
583
  useEffect(() => {
568
584
  if (field?.options?.dataSource === CANVAS_TITLE) return // instead, look-up while typing against Canvas Search API
585
+ if (field?.options?.dataSource === BRIDGE_TITLE) return // instead, look-up while typing against Bridge Search API
569
586
  if (loadRef.current) return
570
587
  loadRef.current = true
571
588
 
@@ -587,25 +604,32 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
587
604
 
588
605
  const searchRef = useRef(query)
589
606
  useEffect(() => {
590
- if (field?.options?.dataSource !== CANVAS_TITLE) { return }
607
+ if (field?.options?.dataSource !== CANVAS_TITLE && field?.options?.dataSource !== BRIDGE_TITLE) { return }
591
608
  if (!query) return
592
609
  if (searchRef.current === query) return
593
610
  searchRef.current = query
594
611
 
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)
612
+ const integration = field?.options?.dataSource === CANVAS_TITLE ? CANVAS_TITLE : BRIDGE_TITLE
613
+ const type = field?.options?.dataSource === CANVAS_TITLE ? 'organizations' : 'payers'
614
+
615
+ const t = setTimeout(() => (
616
+ session.api.integrations.proxy_read({
617
+ integration,
618
+ query,
619
+ type,
620
+ })
621
+ .then(({ data }) => {
622
+ try {
623
+ setPayers(data.map((d: any) => ({
624
+ id: field?.options?.dataSource === CANVAS_TITLE ? d.resource.id : d.id,
625
+ name: field?.options?.dataSource === CANVAS_TITLE ? d.resource.name : d.name,
626
+ })))
627
+ } catch(err) { console.error }
628
+ })
629
+ .catch(console.error)
630
+ ), 300)
631
+
632
+ return () => { clearTimeout(t) }
609
633
  }, [session, field?.options?.dataSource, query])
610
634
 
611
635
  return (
@@ -639,15 +663,15 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
639
663
  }
640
664
  }
641
665
  renderInput={(params) => (
642
- <TextField {...params} InputProps={{ ...params.InputProps, sx: defaultInputProps.sx }}
666
+ <TextField {...params} InputProps={{ ...params.InputProps, sx: (inputProps || defaultInputProps).sx }}
643
667
  required={!field.isOptional} size="small" label={"Insurer"}
644
- placeholder={field.options?.dataSource === CANVAS_TITLE ? "Search insurer..." : "Insurer"}
668
+ placeholder={(field.options?.dataSource === CANVAS_TITLE || field.options?.dataSource === BRIDGE_TITLE) ? "Search insurer..." : "Insurer"}
645
669
  />
646
670
  )}
647
671
  />
648
672
  </Grid>
649
673
  <Grid item xs={12} sm={6}>
650
- <TextField InputProps={defaultInputProps} required={!field.isOptional} fullWidth value={value?.memberId ?? ''}
674
+ <TextField InputProps={inputProps || defaultInputProps} required={!field.isOptional} fullWidth value={value?.memberId ?? ''}
651
675
  onChange={e => onChange({ ...value, memberId: e.target.value }, field.id)}
652
676
  label={form_display_text_for_language(form, "Member ID", '')}
653
677
  size="small"
@@ -655,8 +679,8 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
655
679
  </Grid>
656
680
 
657
681
  <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)}
682
+ <TextField InputProps={inputProps || defaultInputProps} required={false} fullWidth value={value?.planName ?? ''}
683
+ onChange={e => onChange({ ...value, planName: e.target.value }, field.id)}
660
684
  label={form_display_text_for_language(form, "Plan Name", '')}
661
685
  size="small"
662
686
  />
@@ -664,14 +688,15 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
664
688
 
665
689
  <Grid item xs={12} sm={6}>
666
690
  <DateStringInput size="small" label="Plan Start Date"
691
+ inputProps={inputProps}
667
692
  field={{
668
693
  ...field,
669
694
  isOptional: true, //field.isOptional || field.options?.billingProvider === 'Candid'
670
- }}
671
- value={value?.startDate || ''}
672
- onChange={startDate =>
673
- onChange({
674
- ...value,
695
+ }}
696
+ value={value?.startDate || ''}
697
+ onChange={startDate =>
698
+ onChange({
699
+ ...value,
675
700
  startDate,
676
701
  }, field.id)
677
702
  }
@@ -680,7 +705,7 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
680
705
 
681
706
  {field.options?.includeGroupNumber &&
682
707
  <Grid item xs={12}>
683
- <TextField InputProps={defaultInputProps} fullWidth value={value?.groupNumber ?? ''}
708
+ <TextField InputProps={inputProps || defaultInputProps} fullWidth value={value?.groupNumber ?? ''}
684
709
  onChange={e => onChange({ ...value, groupNumber: e.target.value }, field.id)}
685
710
  label={form_display_text_for_language(form, "Group Number", '')}
686
711
  size="small"
@@ -690,16 +715,17 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
690
715
 
691
716
  <Grid item xs={12}>
692
717
  <StringSelector size="small" label="Relationship to Policy Owner"
718
+ inputProps={inputProps}
693
719
  options={
694
720
  (
695
721
  (field.options?.billingProvider === CANVAS_TITLE || field.options?.dataSource === CANVAS_TITLE )
696
- ? INSURANCE_RELATIONSHIPS_CANVAS
722
+ ? INSURANCE_RELATIONSHIPS_CANVAS
697
723
  : INSURANCE_RELATIONSHIPS
698
724
  )
699
725
  .sort((x, y) => x.localeCompare(y))
700
726
  }
701
- value={value?.relationship || 'Self'}
702
- onChange={relationship =>
727
+ value={value?.relationship || 'Self'}
728
+ onChange={relationship =>
703
729
  onChange({ ...value, relationship: relationship as InsuranceRelationship || 'Self' }, field.id)
704
730
  }
705
731
  />
@@ -712,24 +738,24 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
712
738
  </Grid>
713
739
 
714
740
  <Grid item xs={6}>
715
- <TextField label="First Name" size="small" InputProps={defaultInputProps} fullWidth
716
- value={value?.relationshipDetails?.fname || ''}
741
+ <TextField label="First Name" size="small" InputProps={inputProps || defaultInputProps} fullWidth
742
+ value={value?.relationshipDetails?.fname || ''}
717
743
  required={!field.isOptional}
718
- onChange={e =>
719
- onChange({
720
- ...value,
744
+ onChange={e =>
745
+ onChange({
746
+ ...value,
721
747
  relationshipDetails: { ...value?.relationshipDetails, fname: e.target.value }
722
748
  }, field.id)
723
749
  }
724
750
  />
725
751
  </Grid>
726
752
  <Grid item xs={6}>
727
- <TextField label="Last Name" size="small" InputProps={defaultInputProps} fullWidth
728
- value={value?.relationshipDetails?.lname || ''}
753
+ <TextField label="Last Name" size="small" InputProps={inputProps || defaultInputProps} fullWidth
754
+ value={value?.relationshipDetails?.lname || ''}
729
755
  required={!field.isOptional}
730
- onChange={e =>
731
- onChange({
732
- ...value,
756
+ onChange={e =>
757
+ onChange({
758
+ ...value,
733
759
  relationshipDetails: { ...value?.relationshipDetails, lname: e.target.value }
734
760
  }, field.id)
735
761
  }
@@ -737,11 +763,12 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
737
763
  </Grid>
738
764
  <Grid item xs={6}>
739
765
  <StringSelector options={TELLESCOPE_GENDERS} size="small" label="Gender"
740
- value={value?.relationshipDetails?.gender || ''}
766
+ inputProps={inputProps}
767
+ value={value?.relationshipDetails?.gender || ''}
741
768
  required={!field.isOptional}
742
- onChange={v =>
743
- onChange({
744
- ...value,
769
+ onChange={v =>
770
+ onChange({
771
+ ...value,
745
772
  relationshipDetails: { ...value?.relationshipDetails, gender: v as TellescopeGender }
746
773
  }, field.id)
747
774
  }
@@ -749,14 +776,15 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
749
776
  </Grid>
750
777
  <Grid item xs={6}>
751
778
  <DateStringInput size="small" label="Date of Birth"
779
+ inputProps={inputProps}
752
780
  field={{
753
781
  ...field,
754
782
  isOptional: field.isOptional || field.options?.billingProvider === 'Candid'
755
- }}
756
- value={value?.relationshipDetails?.dateOfBirth || ''}
757
- onChange={dateOfBirth =>
758
- onChange({
759
- ...value,
783
+ }}
784
+ value={value?.relationshipDetails?.dateOfBirth || ''}
785
+ onChange={dateOfBirth =>
786
+ onChange({
787
+ ...value,
760
788
  relationshipDetails: { ...value?.relationshipDetails, dateOfBirth }
761
789
  }, field.id)
762
790
  }
@@ -894,8 +922,8 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
894
922
  field.id
895
923
  )}
896
924
  renderInput={(params) => (
897
- <TextField {...params} InputProps={{ ...params.InputProps, sx: defaultInputProps.sx }}
898
- size={'small'} label={"State"} required={!field.isOptional}
925
+ <TextField {...params} InputProps={{ ...params.InputProps, sx: (inputProps || defaultInputProps).sx }}
926
+ size={'small'} label={"State"} required={!field.isOptional}
899
927
  />
900
928
  )}
901
929
  {...props}
@@ -908,7 +936,7 @@ export const InsuranceInput = ({ field, onDatabaseSelect, value, onChange, form,
908
936
  }
909
937
 
910
938
 
911
- const StringSelector = ({ options, value, onChange, required, getDisplayValue, ...props } : {
939
+ const StringSelector = ({ options, value, onChange, required, getDisplayValue, inputProps, ...props } : {
912
940
  options: string[]
913
941
  value: string,
914
942
  onChange: (v: string) => void,
@@ -917,11 +945,12 @@ const StringSelector = ({ options, value, onChange, required, getDisplayValue, .
917
945
  required?: boolean,
918
946
  getDisplayValue?: (v: string) => string,
919
947
  disabled?: boolean,
948
+ inputProps?: { sx: SxProps },
920
949
  }) => (
921
950
  <FormControl fullWidth size={props.size} required={required}>
922
951
  <InputLabel>{props.label}</InputLabel>
923
952
  <Select {...props} value={value} onChange={e => onChange(e.target.value)} fullWidth
924
- sx={defaultInputProps.sx}
953
+ sx={(inputProps || defaultInputProps).sx}
925
954
  >
926
955
  {options.map((o, i) => (
927
956
  <MenuItem value={o} key={o || i}>{getDisplayValue?.(o) ?? o}</MenuItem>
@@ -930,9 +959,354 @@ const StringSelector = ({ options, value, onChange, required, getDisplayValue, .
930
959
  </FormControl>
931
960
  )
932
961
 
962
+ export const BridgeEligibilityInput = ({ field, value, onChange, responses, enduser, inputProps, enduserId, ...props }: FormInputProps<'Bridge Eligibility'> & {
963
+ inputProps?: { sx: SxProps },
964
+ }) => {
965
+ const session = useResolvedSession()
966
+ const [loading, setLoading] = useState(false)
967
+ const [polling, setPolling] = useState(false)
968
+ const [error, setError] = useState<string>()
969
+
970
+ // single-page form must require button-click to check, but 1-page-at-a-time enduser sessions should auto-check
971
+ const isEnduserSession = session.type === 'enduser'
972
+ const eligibilityType = field.options?.bridgeEligibilityType || 'Soft'
973
+
974
+ // Extract payerId from Insurance question response
975
+ const [payerId, memberId, payerName] = useMemo(() => {
976
+ const insuranceResponse = responses?.find(r => r.answer?.type === 'Insurance' && r.answer?.value?.payerId)
977
+ if (insuranceResponse?.answer?.type === 'Insurance') {
978
+ return [
979
+ insuranceResponse.answer.value?.payerId,
980
+ insuranceResponse.answer.value?.memberId,
981
+ insuranceResponse.answer.value?.payerName,
982
+ ]
983
+ }
984
+ // existing payer id is automatically resolved on the backend as default
985
+ return []
986
+ }, [responses])
987
+
988
+ // Extract state from Address question or enduser
989
+ const state = useMemo(() => {
990
+ // Find Address field with state value
991
+ const addressResponse = responses?.find(r =>
992
+ r.answer?.type === 'Address' && r.answer?.value?.state
993
+ )
994
+ if (addressResponse?.answer?.type === 'Address') {
995
+ return addressResponse.answer.value?.state
996
+ }
997
+ // enduser state is automatically resolved on the backend as default
998
+ }, [responses])
999
+
1000
+ // Soft eligibility check function
1001
+ const checkProviderEligibility = useCallback(async () => {
1002
+ const serviceTypeId = field.options?.bridgeServiceTypeId
1003
+
1004
+ if (!serviceTypeId) {
1005
+ setError('Bridge Service Type ID not configured')
1006
+ return
1007
+ }
1008
+ // payerId and state can be automatically resolved on the backend, if already saved on Enduser, so not required here
1009
+
1010
+ setLoading(true)
1011
+ setError(undefined)
1012
+
1013
+ try {
1014
+ const { data } = await session.api.integrations.proxy_read({
1015
+ id: enduserId,
1016
+ integration: BRIDGE_TITLE,
1017
+ type: 'provider-eligibility',
1018
+ query: JSON.stringify({
1019
+ serviceTypeId,
1020
+ payerId,
1021
+ state,
1022
+ }),
1023
+ })
1024
+
1025
+ // Store userIds in shared variable for Appointment Booking to use
1026
+ const userIds = data?.userIds || []
1027
+ setBridgeEligibilityUserIds(userIds)
1028
+
1029
+ // Update the answer with the eligibility result
1030
+ onChange({
1031
+ status: data?.status || 'unknown',
1032
+ userIds,
1033
+ }, field.id)
1034
+ } catch (err: any) {
1035
+ setError(err?.message || 'Failed to check eligibility')
1036
+ console.error('Provider eligibility check failed:', err)
1037
+ } finally {
1038
+ setLoading(false)
1039
+ }
1040
+ }, [session, field, payerId, state, onChange, enduserId])
1041
+
1042
+ // Hard eligibility check function with polling
1043
+ const checkServiceEligibility = useCallback(async () => {
1044
+ const serviceTypeId = field.options?.bridgeServiceTypeId
1045
+
1046
+ if (!serviceTypeId) {
1047
+ setError('Bridge Service Type ID not configured')
1048
+ return
1049
+ }
1050
+
1051
+ setLoading(true)
1052
+ setError(undefined)
1053
+
1054
+ try {
1055
+ // Initiate service eligibility check
1056
+ const { data } = await session.api.integrations.proxy_read({
1057
+ id: enduserId,
1058
+ integration: BRIDGE_TITLE,
1059
+ type: 'service-eligibility',
1060
+ query: JSON.stringify({
1061
+ serviceTypeId,
1062
+ payerId,
1063
+ memberId,
1064
+ state,
1065
+ }),
1066
+ })
1067
+
1068
+ const serviceEligibilityId = data?.id
1069
+ if (!serviceEligibilityId) {
1070
+ throw new Error('No service eligibility ID returned')
1071
+ }
1072
+
1073
+ setLoading(false)
1074
+ setPolling(true)
1075
+
1076
+ // Poll for results
1077
+ const pollForResults = async () => {
1078
+ const maxAttempts = 60 // Poll for up to 60 attempts (2 minutes at 2s intervals)
1079
+ let attempts = 0
1080
+
1081
+ const poll = async (): Promise<void> => {
1082
+ if (attempts >= maxAttempts) {
1083
+ setError('Eligibility check timed out. Please try again.')
1084
+ setPolling(false)
1085
+ return
1086
+ }
1087
+
1088
+ attempts++
1089
+
1090
+ try {
1091
+ const { data: pollData } = await session.api.integrations.proxy_read({
1092
+ id: serviceEligibilityId,
1093
+ integration: BRIDGE_TITLE,
1094
+ type: 'service-eligibility-poll',
1095
+ })
1096
+
1097
+ const status = pollData?.status
1098
+
1099
+ // Check if we're in a terminal state
1100
+ if (status && status !== 'PENDING') {
1101
+ // Store userIds in shared variable for Appointment Booking to use
1102
+ const userIds = pollData?.userIds || []
1103
+ setBridgeEligibilityUserIds(userIds)
1104
+
1105
+ // Update the answer with the eligibility result
1106
+ onChange({
1107
+ status: status || 'unknown',
1108
+ userIds,
1109
+ }, field.id)
1110
+
1111
+ setPolling(false)
1112
+ return
1113
+ }
1114
+
1115
+ // Still pending, poll again after delay
1116
+ setTimeout(poll, 2000) // Poll every 2 seconds
1117
+ } catch (err: any) {
1118
+ setError(err?.message || 'Failed to poll eligibility status')
1119
+ console.error('Service eligibility polling failed:', err)
1120
+ setPolling(false)
1121
+ }
1122
+ }
1123
+
1124
+ poll()
1125
+ }
1126
+
1127
+ pollForResults()
1128
+ } catch (err: any) {
1129
+ setError(err?.message || 'Failed to check service eligibility')
1130
+ console.error('Service eligibility check failed:', err)
1131
+ setLoading(false)
1132
+ setPolling(false)
1133
+ }
1134
+ }, [session, field, payerId, memberId, state, onChange, enduserId])
1135
+
1136
+ // Auto-check eligibility for enduser sessions
1137
+ const autoCheckRef = useRef(false)
1138
+ useEffect(() => {
1139
+ if (!isEnduserSession) return
1140
+ if (autoCheckRef.current) return
1141
+ autoCheckRef.current = true
1142
+
1143
+ if (eligibilityType === 'Hard') {
1144
+ checkServiceEligibility()
1145
+ } else {
1146
+ checkProviderEligibility()
1147
+ }
1148
+ }, [isEnduserSession, eligibilityType, checkProviderEligibility, checkServiceEligibility])
1149
+
1150
+ const errorComponent = useMemo(() => (
1151
+ <Grid container spacing={2} direction="column" alignItems="center" style={{ padding: '20px 0' }}>
1152
+ <Grid item>
1153
+ <Paper style={{
1154
+ padding: 16,
1155
+ backgroundColor: '#ffebee',
1156
+ border: '2px solid #f44336'
1157
+ }}>
1158
+ <Grid container spacing={2} direction="column" alignItems="center">
1159
+ <Grid item>
1160
+ <Typography variant="h2" style={{ color: '#f44336' }}>⚠️</Typography>
1161
+ </Grid>
1162
+ <Grid item>
1163
+ <Typography variant="h6" align="center" color="error">
1164
+ Unable to Check Eligibility
1165
+ </Typography>
1166
+ </Grid>
1167
+ <Grid item>
1168
+ <Typography variant="body2" align="center" style={{ color: '#d32f2f' }}>
1169
+ {error}
1170
+ </Typography>
1171
+ </Grid>
1172
+ </Grid>
1173
+ </Paper>
1174
+ </Grid>
1175
+ </Grid>
1176
+ ), [error])
1177
+
1178
+ const checkingEligibilityComponent = useMemo(() => (
1179
+ <Grid container spacing={2} direction="column" alignItems="center" style={{ padding: '20px 0' }}>
1180
+ <Grid item>
1181
+ <CircularProgress size={40} />
1182
+ </Grid>
1183
+ <Grid item>
1184
+ <Typography variant="body1">
1185
+ {polling ? 'Verifying eligibility with insurance...' : 'Checking eligibility...'}
1186
+ </Typography>
1187
+ </Grid>
1188
+ <Grid item>
1189
+ <Typography variant="body2" color="textSecondary">
1190
+ {polling ? 'This usually takes 15-30 seconds' : 'This may take a few moments'}
1191
+ </Typography>
1192
+ </Grid>
1193
+ </Grid>
1194
+ ), [polling])
1195
+
1196
+ const resultsComponent = useMemo(() => {
1197
+ const isEligible = value?.status === 'ELIGIBLE'
1198
+ return (
1199
+ <Grid container spacing={2} direction="column">
1200
+ <Grid item>
1201
+ <Paper style={{
1202
+ padding: 16,
1203
+ backgroundColor: isEligible ? '#e8f5e9' : '#fff3e0',
1204
+ border: `2px solid ${isEligible ? '#4caf50' : '#ff9800'}`
1205
+ }}>
1206
+ <Grid container spacing={2} direction="column" alignItems="center">
1207
+ <Grid item>
1208
+ {isEligible ? (
1209
+ <CheckCircleOutline style={{ fontSize: 48, color: '#4caf50' }} />
1210
+ ) : (
1211
+ <Typography variant="h2" style={{ color: '#ff9800' }}>⚠️</Typography>
1212
+ )}
1213
+ </Grid>
1214
+ <Grid item>
1215
+ <Typography variant="h6" align="center">
1216
+ {isEligible
1217
+ ? `${payerName || 'Your insurance provider'} is accepted!`
1218
+ : 'Eligibility Status: ' + first_letter_capitalized((value?.status || 'Unknown').toLowerCase())
1219
+ }
1220
+ </Typography>
1221
+ </Grid>
1222
+ </Grid>
1223
+ </Paper>
1224
+ </Grid>
1225
+ </Grid>
1226
+ )
1227
+ }, [value])
1228
+
1229
+ // Loading/polling state for enduser sessions
1230
+ if (isEnduserSession) {
1231
+ if (loading || polling) { return checkingEligibilityComponent }
1232
+ if (error) {
1233
+ return errorComponent
1234
+ }
1235
+ if (value?.status) {
1236
+ return resultsComponent
1237
+ }
1238
+ return errorComponent
1239
+ }
1240
+
1241
+ // User/admin interface (non-enduser sessions)
1242
+ return (
1243
+ <Grid container spacing={2} direction="column">
1244
+ <Grid item>
1245
+ <Typography variant="body2" color="textSecondary">
1246
+ Eligibility Type: {eligibilityType}
1247
+ </Typography>
1248
+ <Typography variant="body2" color="textSecondary">
1249
+ Service Type: {field.options?.bridgeServiceTypeId || 'Not configured'}
1250
+ </Typography>
1251
+ {state && <Typography variant="body2" color="textSecondary">State: {state}</Typography>}
1252
+ {payerId && <Typography variant="body2" color="textSecondary">Payer ID: {payerId}</Typography>}
1253
+ {memberId && <Typography variant="body2" color="textSecondary">Member ID: {memberId}</Typography>}
1254
+ </Grid>
1255
+
1256
+ {error && (
1257
+ <Grid item>
1258
+ <Typography variant="body2" color="error">{error}</Typography>
1259
+ </Grid>
1260
+ )}
1261
+
1262
+ {polling && (
1263
+ <Grid item>
1264
+ <Typography variant="body2" color="primary">
1265
+ Polling for results... (this may take 15-30 seconds)
1266
+ </Typography>
1267
+ </Grid>
1268
+ )}
1269
+
1270
+ <Grid item container spacing={2}>
1271
+ <Grid item>
1272
+ <LoadingButton
1273
+ variant="outlined"
1274
+ onClick={checkProviderEligibility}
1275
+ submitText="Check Provider Eligibility (Free)"
1276
+ submittingText="Checking..."
1277
+ submitting={loading && !polling}
1278
+ disabled={!field.options?.bridgeServiceTypeId || loading || polling}
1279
+ />
1280
+ </Grid>
1281
+ <Grid item>
1282
+ <LoadingButton
1283
+ variant="outlined"
1284
+ onClick={checkServiceEligibility}
1285
+ submitText="Check Service Eligibility (Paid)"
1286
+ submittingText={polling ? "Polling..." : "Initiating..."}
1287
+ submitting={loading || polling}
1288
+ disabled={!field.options?.bridgeServiceTypeId || loading || polling}
1289
+ />
1290
+ </Grid>
1291
+ </Grid>
1292
+
1293
+ {value && (
1294
+ <Grid item>
1295
+ <Typography variant="caption" color="textSecondary">
1296
+ Current Answer:
1297
+ </Typography>
1298
+ <pre style={{ fontSize: 11, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
1299
+ {JSON.stringify(value, null, 2)}
1300
+ </pre>
1301
+ </Grid>
1302
+ )}
1303
+ </Grid>
1304
+ )
1305
+ }
1306
+
933
1307
  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())}
1308
+ <StringSelector {...props}
1309
+ options={Array(12).fill('').map((_, i) => (i + 1) <= 9 ? `0${i + 1}` : (i + 1).toString())}
936
1310
  />
937
1311
  )
938
1312
  const MinuteSelector = (props : { value: string, onChange: (v: string) => void }) => (
@@ -3685,6 +4059,16 @@ export const AppointmentBookingInput = ({ formResponseId, field, value, onChange
3685
4059
  .join(',')
3686
4060
  }`
3687
4061
  }
4062
+ // Filter to Bridge eligibility userIds if option is enabled
4063
+ if (field.options?.useBridgeEligibilityResult) {
4064
+ const bridgeUserIds = getBridgeEligibilityUserIds()
4065
+
4066
+ if (bridgeUserIds.length === 0) {
4067
+ return <Typography>No eligible users found for booking</Typography>
4068
+ }
4069
+
4070
+ bookingURL += `&userIds=${bridgeUserIds.join(',')}`
4071
+ }
3688
4072
  // need to use form?.id for internally-submitted forms because formResponseId isn't generated until initial submission or saved draft
3689
4073
  if (field.options?.holdAppointmentMinutes && (formResponseId || field?.id)) {
3690
4074
  bookingURL += `&formResponseId=${formResponseId || field?.id}`