@tellescope/react-components 1.243.1 → 1.244.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 (65) hide show
  1. package/lib/cjs/Forms/forms.d.ts +1 -0
  2. package/lib/cjs/Forms/forms.d.ts.map +1 -1
  3. package/lib/cjs/Forms/forms.js +39 -37
  4. package/lib/cjs/Forms/forms.js.map +1 -1
  5. package/lib/cjs/Forms/forms.v2.d.ts +1 -0
  6. package/lib/cjs/Forms/forms.v2.d.ts.map +1 -1
  7. package/lib/cjs/Forms/forms.v2.js +47 -40
  8. package/lib/cjs/Forms/forms.v2.js.map +1 -1
  9. package/lib/cjs/Forms/hooks.d.ts +1 -0
  10. package/lib/cjs/Forms/hooks.d.ts.map +1 -1
  11. package/lib/cjs/Forms/hooks.js +71 -28
  12. package/lib/cjs/Forms/hooks.js.map +1 -1
  13. package/lib/cjs/Forms/inputs.d.ts +5 -0
  14. package/lib/cjs/Forms/inputs.d.ts.map +1 -1
  15. package/lib/cjs/Forms/inputs.js +202 -12
  16. package/lib/cjs/Forms/inputs.js.map +1 -1
  17. package/lib/cjs/Forms/inputs.v2.d.ts +1 -0
  18. package/lib/cjs/Forms/inputs.v2.d.ts.map +1 -1
  19. package/lib/cjs/Forms/inputs.v2.js +16 -6
  20. package/lib/cjs/Forms/inputs.v2.js.map +1 -1
  21. package/lib/cjs/forms.d.ts +2 -1
  22. package/lib/cjs/forms.d.ts.map +1 -1
  23. package/lib/cjs/forms.js +2 -2
  24. package/lib/cjs/forms.js.map +1 -1
  25. package/lib/cjs/inputs_shared.d.ts +1 -0
  26. package/lib/cjs/inputs_shared.d.ts.map +1 -1
  27. package/lib/cjs/inputs_shared.js +27 -3
  28. package/lib/cjs/inputs_shared.js.map +1 -1
  29. package/lib/esm/Forms/forms.d.ts +1 -0
  30. package/lib/esm/Forms/forms.d.ts.map +1 -1
  31. package/lib/esm/Forms/forms.js +40 -38
  32. package/lib/esm/Forms/forms.js.map +1 -1
  33. package/lib/esm/Forms/forms.v2.d.ts +1 -0
  34. package/lib/esm/Forms/forms.v2.d.ts.map +1 -1
  35. package/lib/esm/Forms/forms.v2.js +48 -41
  36. package/lib/esm/Forms/forms.v2.js.map +1 -1
  37. package/lib/esm/Forms/hooks.d.ts +1 -0
  38. package/lib/esm/Forms/hooks.d.ts.map +1 -1
  39. package/lib/esm/Forms/hooks.js +70 -28
  40. package/lib/esm/Forms/hooks.js.map +1 -1
  41. package/lib/esm/Forms/inputs.d.ts +5 -0
  42. package/lib/esm/Forms/inputs.d.ts.map +1 -1
  43. package/lib/esm/Forms/inputs.js +202 -13
  44. package/lib/esm/Forms/inputs.js.map +1 -1
  45. package/lib/esm/Forms/inputs.v2.d.ts +1 -0
  46. package/lib/esm/Forms/inputs.v2.d.ts.map +1 -1
  47. package/lib/esm/Forms/inputs.v2.js +16 -7
  48. package/lib/esm/Forms/inputs.v2.js.map +1 -1
  49. package/lib/esm/forms.d.ts +2 -1
  50. package/lib/esm/forms.d.ts.map +1 -1
  51. package/lib/esm/forms.js +2 -2
  52. package/lib/esm/forms.js.map +1 -1
  53. package/lib/esm/inputs_shared.d.ts +1 -0
  54. package/lib/esm/inputs_shared.d.ts.map +1 -1
  55. package/lib/esm/inputs_shared.js +28 -4
  56. package/lib/esm/inputs_shared.js.map +1 -1
  57. package/lib/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +9 -9
  59. package/src/Forms/forms.tsx +10 -2
  60. package/src/Forms/forms.v2.tsx +29 -3
  61. package/src/Forms/hooks.tsx +46 -1
  62. package/src/Forms/inputs.tsx +304 -6
  63. package/src/Forms/inputs.v2.tsx +19 -4
  64. package/src/forms.tsx +3 -1
  65. package/src/inputs_shared.tsx +39 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellescope/react-components",
3
- "version": "1.243.1",
3
+ "version": "1.244.0",
4
4
  "description": "",
5
5
  "main": "./lib/cjs/index.js",
6
6
  "module": "./lib/esm/index.js",
@@ -51,13 +51,13 @@
51
51
  "@reduxjs/toolkit": "1.9.0",
52
52
  "@stripe/react-stripe-js": "2.9.0",
53
53
  "@stripe/stripe-js": "1.52.1",
54
- "@tellescope/constants": "1.243.1",
55
- "@tellescope/sdk": "1.243.1",
56
- "@tellescope/types-client": "1.243.1",
57
- "@tellescope/types-models": "1.243.1",
58
- "@tellescope/types-utilities": "1.243.1",
59
- "@tellescope/utilities": "1.243.1",
60
- "@tellescope/validation": "1.243.1",
54
+ "@tellescope/constants": "1.244.0",
55
+ "@tellescope/sdk": "1.244.0",
56
+ "@tellescope/types-client": "1.244.0",
57
+ "@tellescope/types-models": "1.244.0",
58
+ "@tellescope/types-utilities": "1.244.0",
59
+ "@tellescope/utilities": "1.244.0",
60
+ "@tellescope/validation": "1.244.0",
61
61
  "css-to-react-native": "3.0.0",
62
62
  "draft-js": "0.11.7",
63
63
  "draftjs-to-html": "0.9.1",
@@ -84,7 +84,7 @@
84
84
  "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
85
85
  "react-native": "^0.65.0 || ^0.66.0 || ^0.67.0 || ^0.68.0 || ^0.71.0"
86
86
  },
87
- "gitHead": "79743fc317437cf64b992dce8f1a0a4e4a23032c",
87
+ "gitHead": "c524b1c2642980346683d5af72c92f6ebfa17f5f",
88
88
  "publishConfig": {
89
89
  "access": "public"
90
90
  }
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
2
2
  import { Button, CircularProgress, FileBlob, FileUploadHandler, Flex, LinearProgress, LoadingButton, Modal, Paper, Styled, Typography, form_display_text_for_language, useFileUpload, useFormResponses, useSession } from "../index"
3
3
  import { useListForFormFields, useOrganizationTheme, useTellescopeForm, WithOrganizationTheme, Response, FileResponse, NextFieldLogicOptions } from "./hooks"
4
4
  import { ChangeHandler, FormInputs } from "./types"
5
- import { AddToDatabaseProps, AddressInput, AllergiesInput, AppointmentBookingInput, BelugaPatientPreferenceInput, BridgeEligibilityInput, ChargeebeeInput, ConditionsInput, DatabaseSelectInput, DateInput, DateStringInput, DropdownInput, EmailInput, EmotiiInput, FileInput, FilesInput, HeightInput, HiddenValueInput, InsuranceInput, LanguageSelect, MedicationsInput, MultipleChoiceInput, NumberInput, PharmacySearchInput, PhoneInput, Progress, RankingInput, RatingInput, RedirectInput, RelatedContactsInput, RichTextInput, SignatureInput, StringInput, StringLongInput, StripeInput, TableInput, TimeInput, TimezoneInput, defaultButtonStyles } from "./inputs"
5
+ import { AddToDatabaseProps, AddressInput, AllergiesInput, AppointmentBookingInput, BelugaPatientPreferenceInput, BridgeEligibilityInput, CandidEligibilityInput, ChargeebeeInput, ConditionsInput, DatabaseSelectInput, DateInput, DateStringInput, DropdownInput, EmailInput, EmotiiInput, FileInput, FilesInput, HeightInput, HiddenValueInput, InsuranceInput, LanguageSelect, MedicationsInput, MultipleChoiceInput, NumberInput, PharmacySearchInput, PhoneInput, Progress, RankingInput, RatingInput, RedirectInput, RelatedContactsInput, RichTextInput, SignatureInput, StringInput, StringLongInput, StripeInput, TableInput, TimeInput, TimezoneInput, defaultButtonStyles } from "./inputs"
6
6
  import { PRIMARY_HEX } from "@tellescope/constants"
7
7
  import { FormResponse, FormField, Form, Enduser } from "@tellescope/types-client"
8
8
  import { FormResponseAnswerFileValue, OrganizationTheme } from "@tellescope/types-models"
@@ -17,6 +17,7 @@ export const TellescopeFormContainer = ({ businessId, organizationIds, ...props
17
17
  hideBg?: boolean,
18
18
  backgroundColor?: string,
19
19
  hideLogo?: boolean,
20
+ showLogo?: boolean,
20
21
  logoURL?: string,
21
22
  logoHeight?: number,
22
23
  language?: string,
@@ -186,6 +187,7 @@ export const QuestionForField = ({
186
187
  const RelatedContacts = customInputs?.['Related Contacts'] ?? RelatedContactsInput
187
188
  const Insurance = customInputs?.['Insurance'] ?? InsuranceInput
188
189
  const BridgeEligibility = customInputs?.['Bridge Eligibility'] ?? BridgeEligibilityInput
190
+ const CandidEligibility = customInputs?.['Candid Eligibility'] ?? CandidEligibilityInput
189
191
  const AppointmentBooking = customInputs?.['Appointment Booking'] ?? AppointmentBookingInput
190
192
  const Height = customInputs?.['Height'] ?? HeightInput
191
193
  const Redirect = customInputs?.['Redirect'] ?? RedirectInput
@@ -225,7 +227,7 @@ export const QuestionForField = ({
225
227
  fontSize: field.titleFontSize || (field.type === 'Question Group' ? 22 : 20),
226
228
  fontWeight: field.type === 'Question Group' ? 'bold' : undefined,
227
229
  }}>
228
- {field.title}{!(field.isOptional || field.type === 'description' || field.type === 'Question Group' || field.type === 'Insurance' || field.type === 'Bridge Eligibility') ? '*' : ''}
230
+ {field.title}{!(field.isOptional || field.type === 'description' || field.type === 'Question Group' || field.type === 'Insurance' || field.type === 'Bridge Eligibility' || field.type === 'Candid Eligibility') ? '*' : ''}
229
231
  </Typography>
230
232
  }
231
233
  {!field.title && (field.type === 'Question Group' || field.type === 'signature') && !form?.customization?.hideLogo &&
@@ -382,6 +384,12 @@ export const QuestionForField = ({
382
384
  onChange={onFieldChange as ChangeHandler<'Bridge Eligibility'>}
383
385
  />
384
386
  )
387
+ : field.type === 'Candid Eligibility' ? (
388
+ <CandidEligibility field={field} value={value.answer.value as any} form={form}
389
+ enduser={enduser} responses={responses} enduserId={enduserId}
390
+ onChange={onFieldChange as ChangeHandler<'Candid Eligibility'>}
391
+ />
392
+ )
385
393
  : field.type === 'Pharmacy Search' ? (
386
394
  <PharmacySearch field={field} value={value.answer.value as any} form={form}
387
395
  enduser={enduser} responses={responses}
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
2
2
  import { Button, CircularProgress, FileBlob, FileUploadHandler, Flex, LinearProgress, LoadingButton, Modal, Paper, Styled, Typography, WithTheme, form_display_text_for_language, useFileUpload, useFormResponses, useSession } from "../index"
3
3
  import { useListForFormFields, useOrganizationTheme, useTellescopeForm, WithOrganizationTheme, Response, FileResponse, NextFieldLogicOptions } from "./hooks"
4
4
  import { ChangeHandler, FormInputs } from "./types"
5
- import { AddToDatabaseProps, AddressInput, AllergiesInput, AppointmentBookingInput, BelugaPatientPreferenceInput, BridgeEligibilityInput, ChargeebeeInput, ConditionsInput, DatabaseSelectInput, DateInput, DateStringInput, DropdownInput, EmailInput, EmotiiInput, FileInput, FilesInput, HeightInput, HiddenValueInput, InsuranceInput, LanguageSelect, MedicationsInput, MultipleChoiceInput, NumberInput, PharmacySearchInput, PhoneInput, Progress, RankingInput, RatingInput, RedirectInput, RelatedContactsInput, RichTextInput, SignatureInput, StringInput, StringLongInput, StripeInput, TableInput, TimeInput, TimezoneInput, defaultButtonStyles } from "./inputs.v2"
5
+ import { AddToDatabaseProps, AddressInput, AllergiesInput, AppointmentBookingInput, BelugaPatientPreferenceInput, BridgeEligibilityInput, CandidEligibilityInput, ChargeebeeInput, ConditionsInput, DatabaseSelectInput, DateInput, DateStringInput, DropdownInput, EmailInput, EmotiiInput, FileInput, FilesInput, HeightInput, HiddenValueInput, InsuranceInput, LanguageSelect, MedicationsInput, MultipleChoiceInput, NumberInput, PharmacySearchInput, PhoneInput, Progress, RankingInput, RatingInput, RedirectInput, RelatedContactsInput, RichTextInput, SignatureInput, StringInput, StringLongInput, StripeInput, TableInput, TimeInput, TimezoneInput, defaultButtonStyles } from "./inputs.v2"
6
6
  import { PRIMARY_HEX } from "@tellescope/constants"
7
7
  import { FormResponse, FormField, Form, Enduser } from "@tellescope/types-client"
8
8
  import { FormResponseAnswerFileValue, OrganizationTheme } from "@tellescope/types-models"
@@ -17,6 +17,7 @@ export const TellescopeFormContainerV2 = ({ businessId, organizationIds, ...prop
17
17
  hideBg?: boolean,
18
18
  backgroundColor?: string,
19
19
  hideLogo?: boolean,
20
+ showLogo?: boolean,
20
21
  logoURL?: string,
21
22
  logoHeight?: number,
22
23
  language?: string,
@@ -36,7 +37,7 @@ export const TellescopeFormContainerV2 = ({ businessId, organizationIds, ...prop
36
37
  )
37
38
  }
38
39
 
39
- const TellescopeFormContainerWithThemeV2: typeof TellescopeFormContainerV2 = ({ paperMinHeight=575, children, language, onChangeLanguage, style, hideBg, backgroundColor, hideLogo, logoHeight, maxWidth }) => {
40
+ const TellescopeFormContainerWithThemeV2: typeof TellescopeFormContainerV2 = ({ paperMinHeight=575, children, language, onChangeLanguage, style, hideBg, backgroundColor, hideLogo, showLogo, logoURL, logoHeight, maxWidth }) => {
40
41
  const theme = useOrganizationTheme()
41
42
 
42
43
  // V2: No paper background by default, cleaner layout with light blue background
@@ -44,10 +45,13 @@ const TellescopeFormContainerWithThemeV2: typeof TellescopeFormContainerV2 = ({
44
45
  const shouldUseCustomBg = backgroundColor && backgroundColor !== theme.themeColor && backgroundColor !== '#ffffff'
45
46
  const finalBgColor = shouldUseCustomBg ? backgroundColor : '#F4F3FA'
46
47
 
48
+ const resolvedLogoURL = logoURL || theme?.logoURL
49
+
47
50
  return (
48
51
  <Flex flex={1} column alignItems="center" style={{
49
52
  backgroundColor: finalBgColor,
50
53
  overflow: 'auto',
54
+ boxSizing: 'border-box',
51
55
  paddingTop: window.innerWidth < 600 ? 20 : 40,
52
56
  paddingBottom: window.innerWidth < 600 ? 20 : 40,
53
57
  ...style
@@ -68,6 +72,21 @@ const TellescopeFormContainerWithThemeV2: typeof TellescopeFormContainerV2 = ({
68
72
  <LanguageSelect value={language} onChange={onChangeLanguage} />
69
73
  </Flex>
70
74
  }
75
+ {showLogo && !hideLogo && (
76
+ resolvedLogoURL
77
+ ? (
78
+ <Flex alignItems="flex-start" style={{ marginBottom: '20px', marginTop: '10px' }}>
79
+ <img src={resolvedLogoURL} alt={theme?.name} style={{ height: logoHeight || LOGO_HEIGHT, maxWidth: 225 }} />
80
+ </Flex>
81
+ )
82
+ : theme?.name
83
+ ? (
84
+ <Typography style={{ fontSize: 22, marginBottom: '20px', marginTop: '10px', textAlign: 'left', fontWeight: 600 }}>
85
+ {theme.name}
86
+ </Typography>
87
+ )
88
+ : null
89
+ )}
71
90
  {children}
72
91
  </Flex>
73
92
  </Flex>
@@ -177,6 +196,7 @@ export const QuestionForField = ({
177
196
  const RelatedContacts = customInputs?.['Related Contacts'] ?? RelatedContactsInput
178
197
  const Insurance = customInputs?.['Insurance'] ?? InsuranceInput
179
198
  const BridgeEligibility = customInputs?.['Bridge Eligibility'] ?? BridgeEligibilityInput
199
+ const CandidEligibility = customInputs?.['Candid Eligibility'] ?? CandidEligibilityInput
180
200
  const AppointmentBooking = customInputs?.['Appointment Booking'] ?? AppointmentBookingInput
181
201
  const Height = customInputs?.['Height'] ?? HeightInput
182
202
  const Redirect = customInputs?.['Redirect'] ?? RedirectInput
@@ -217,7 +237,7 @@ export const QuestionForField = ({
217
237
  fontSize: field.titleFontSize || (field.type === 'Question Group' ? 22 : 20),
218
238
  fontWeight: field.type === 'Question Group' ? 'bold' : undefined,
219
239
  }}>
220
- {field.title}{!(field.isOptional || field.type === 'description' || field.type === 'Question Group' || field.type === 'Insurance' || field.type === 'Bridge Eligibility') ? '*' : ''}
240
+ {field.title}{!(field.isOptional || field.type === 'description' || field.type === 'Question Group' || field.type === 'Insurance' || field.type === 'Bridge Eligibility' || field.type === 'Candid Eligibility') ? '*' : ''}
221
241
  </Typography>
222
242
  }
223
243
  {!field.title && (field.type === 'Question Group' || field.type === 'signature') && !form?.customization?.hideLogo &&
@@ -374,6 +394,12 @@ export const QuestionForField = ({
374
394
  onChange={onFieldChange as ChangeHandler<'Bridge Eligibility'>}
375
395
  />
376
396
  )
397
+ : field.type === 'Candid Eligibility' ? (
398
+ <CandidEligibility field={field} value={value.answer.value as any} form={form}
399
+ enduser={enduser} responses={responses} enduserId={enduserId}
400
+ onChange={onFieldChange as ChangeHandler<'Candid Eligibility'>}
401
+ />
402
+ )
377
403
  : field.type === 'Pharmacy Search' ? (
378
404
  <PharmacySearch field={field} value={value.answer.value as any} form={form}
379
405
  enduser={enduser} responses={responses}
@@ -9,7 +9,13 @@ import { WithTheme, contact_is_valid, useAddGTMTag, useFileUpload, useFormFields
9
9
  import ReactGA from "react-ga4";
10
10
 
11
11
  import isEmail from "validator/lib/isEmail"
12
- import { append_current_utm_params, emit_gtm_event, field_can_autoadvance, getLocalTimezone, get_time_values, get_utm_params, is_object, object_is_empty, read_local_storage, replace_form_field_template_values, responses_satisfy_conditions, update_local_storage } from "@tellescope/utilities"
12
+ import { MM_DD_YYYY_to_YYYY_MM_DD, append_current_utm_params, emit_gtm_event, field_can_autoadvance, getLocalTimezone, get_time_values, get_utm_params, is_object, mm_dd_yyyy, object_is_empty, read_local_storage, replace_form_field_template_values, responses_satisfy_conditions, update_local_storage } from "@tellescope/utilities"
13
+
14
+ export const dateFromOffsetMs = (offsetMs: number): Date => {
15
+ const d = new Date()
16
+ d.setHours(0, 0, 0, 0)
17
+ return new Date(d.getTime() + offsetMs)
18
+ }
13
19
 
14
20
  export const useFlattenedTree = (root?: FormFieldNode) => {
15
21
  const flat: FormField[] = []
@@ -1065,6 +1071,45 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1065
1071
  }
1066
1072
  }
1067
1073
 
1074
+ if (value.answer.type === 'dateString' && value.answer.value) {
1075
+ const dateStr = value.answer.value
1076
+ if (field.options?.minDateOffsetMs !== undefined) {
1077
+ const minDate = dateFromOffsetMs(field.options.minDateOffsetMs)
1078
+ const parsed = new Date(MM_DD_YYYY_to_YYYY_MM_DD(dateStr))
1079
+ parsed.setMinutes(parsed.getMinutes() + parsed.getTimezoneOffset())
1080
+ parsed.setHours(0, 0, 0, 0)
1081
+ if (parsed < minDate) {
1082
+ return `Date must not be earlier than ${mm_dd_yyyy(minDate)}`
1083
+ }
1084
+ }
1085
+ if (field.options?.maxDateOffsetMs !== undefined) {
1086
+ const maxDate = dateFromOffsetMs(field.options.maxDateOffsetMs)
1087
+ const parsed = new Date(MM_DD_YYYY_to_YYYY_MM_DD(dateStr))
1088
+ parsed.setMinutes(parsed.getMinutes() + parsed.getTimezoneOffset())
1089
+ parsed.setHours(0, 0, 0, 0)
1090
+ if (parsed > maxDate) {
1091
+ return `Date must not be later than ${mm_dd_yyyy(maxDate)}`
1092
+ }
1093
+ }
1094
+ }
1095
+
1096
+ if (value.answer.type === 'date' && value.answer.value) {
1097
+ const dateVal = new Date(value.answer.value)
1098
+ if (field.options?.minDateOffsetMs !== undefined) {
1099
+ const minDate = dateFromOffsetMs(field.options.minDateOffsetMs)
1100
+ if (dateVal < minDate) {
1101
+ return `Date must not be earlier than ${mm_dd_yyyy(minDate)}`
1102
+ }
1103
+ }
1104
+ if (field.options?.maxDateOffsetMs !== undefined) {
1105
+ const maxDate = dateFromOffsetMs(field.options.maxDateOffsetMs)
1106
+ maxDate.setHours(23, 59, 59, 999)
1107
+ if (dateVal > maxDate) {
1108
+ return `Date must not be later than ${mm_dd_yyyy(maxDate)}`
1109
+ }
1110
+ }
1111
+ }
1112
+
1068
1113
  if (field.isOptional || (sessionType === 'user' && field.type === 'Appointment Booking' && !enduserId)) {
1069
1114
  return null
1070
1115
  }
@@ -3,7 +3,7 @@ import axios from "axios"
3
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, BRIDGE_TITLE, EMOTII_TITLE, INSURANCE_RELATIONSHIPS, INSURANCE_RELATIONSHIPS_CANVAS, PRIMARY_HEX, RELATIONSHIP_TYPES, TELLESCOPE_GENDERS } from "@tellescope/constants"
6
+ import { CANVAS_TITLE, BRIDGE_TITLE, CANDID_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, Pharmacy, TellescopeGender, TIMEZONES_USA } from "@tellescope/types-models"
9
9
  import { VALID_STATES, emailValidator, phoneValidator } from "@tellescope/validation"
@@ -25,7 +25,7 @@ import { Elements, PaymentElement, useStripe, useElements, EmbeddedCheckout, Emb
25
25
  import { loadStripe } from '@stripe/stripe-js';
26
26
  import { CheckCircleOutline, Delete, Edit, ExpandMore } from "@mui/icons-material"
27
27
  import { WYSIWYG } from "./wysiwyg"
28
- import { useConditionalChoices, Response } from "./hooks"
28
+ import { dateFromOffsetMs, useConditionalChoices, Response } from "./hooks"
29
29
 
30
30
  // Bridge Eligibility - shared variable for storing most recent eligibility userIds
31
31
  const bridgeEligibilityResult = {
@@ -265,14 +265,17 @@ const CustomDateInput = forwardRef((props: TextFieldProps, ref) => (
265
265
  fullWidth inputRef={ref} {...props}
266
266
  />
267
267
  ))
268
- export const DateInput = ({
269
- field, value, onChange, placement='top', ...props
268
+ export const DateInput = ({
269
+ field, value, onChange, placement='top', ...props
270
270
  } : {
271
271
  field: FormField,
272
272
  placement?: 'top' | 'right' | 'bottom' | 'left'
273
273
  } & FormInputProps<'date'> & Styled) => {
274
274
  const inputRef = useRef(null);
275
275
 
276
+ const minDate = field.options?.minDateOffsetMs !== undefined ? dateFromOffsetMs(field.options.minDateOffsetMs) : undefined
277
+ const maxDate = field.options?.maxDateOffsetMs !== undefined ? dateFromOffsetMs(field.options.maxDateOffsetMs) : undefined
278
+
276
279
  return (
277
280
  <DatePicker // wrap in item to prevent movement on focused
278
281
  selected={value}
@@ -286,6 +289,8 @@ export const DateInput = ({
286
289
  customInput={<CustomDateInput inputRef={inputRef} {...props} />}
287
290
  // className={css`width: 100%;`}
288
291
  className={css`${datepickerCSS}`}
292
+ minDate={minDate}
293
+ maxDate={maxDate}
289
294
  />
290
295
  )
291
296
  }
@@ -436,6 +441,9 @@ const CustomDateStringInput = forwardRef((props: TextFieldProps & { inputProps?:
436
441
  export const DateStringInput = ({ field, value, onChange, form, ...props }: FormInputProps<'string'>) => {
437
442
  const inputRef = useRef(null);
438
443
 
444
+ const minDate = field.options?.minDateOffsetMs !== undefined ? dateFromOffsetMs(field.options.minDateOffsetMs) : undefined
445
+ const maxDate = field.options?.maxDateOffsetMs !== undefined ? dateFromOffsetMs(field.options.maxDateOffsetMs) : undefined
446
+
439
447
  // if (value && isDateString(value)) {
440
448
  // console.log(value, new Date(
441
449
  // new Date(MM_DD_YYYY_to_YYYY_MM_DD(value)).getTime()
@@ -459,11 +467,13 @@ export const DateStringInput = ({ field, value, onChange, form, ...props }: Form
459
467
  required={!field.isOptional}
460
468
  autoComplete="off"
461
469
  dateFormat={"MM-dd-yyyy"}
462
- customInput={<CustomDateStringInput inputRef={inputRef} {...props}
463
- label={(!field.title && field.placeholder) ? field.placeholder : props.label}
470
+ customInput={<CustomDateStringInput inputRef={inputRef} {...props}
471
+ label={(!field.title && field.placeholder) ? field.placeholder : props.label}
464
472
  />}
465
473
  // className={css`width: 100%;`}
466
474
  className={css`${datepickerCSS}`}
475
+ minDate={minDate}
476
+ maxDate={maxDate}
467
477
  />
468
478
  )
469
479
  : (
@@ -1418,6 +1428,294 @@ export const BridgeEligibilityInput = ({ field, value, onChange, responses, endu
1418
1428
  )
1419
1429
  }
1420
1430
 
1431
+ export const CandidEligibilityInput = ({ field, value, onChange, responses, enduser, inputProps, enduserId, form, ...props }: FormInputProps<'Candid Eligibility'> & {
1432
+ inputProps?: { sx: SxProps },
1433
+ }) => {
1434
+ const session = useResolvedSession()
1435
+ const [loading, setLoading] = useState(false)
1436
+ const [polling, setPolling] = useState(false)
1437
+ const [error, setError] = useState<string>()
1438
+
1439
+ const isEnduserSession = session.type === 'enduser'
1440
+ const pollTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
1441
+
1442
+ // Clean up polling timeout on unmount
1443
+ useEffect(() => {
1444
+ return () => {
1445
+ if (pollTimeoutRef.current) clearTimeout(pollTimeoutRef.current)
1446
+ }
1447
+ }, [])
1448
+
1449
+ // Extract payerId from Insurance question response
1450
+ const [payerId, memberId, payerName] = useMemo(() => {
1451
+ const insuranceResponse = responses?.find(r => r.answer?.type === 'Insurance' && r.answer?.value?.payerId)
1452
+ if (insuranceResponse?.answer?.type === 'Insurance') {
1453
+ return [
1454
+ insuranceResponse.answer.value?.payerId,
1455
+ insuranceResponse.answer.value?.memberId,
1456
+ insuranceResponse.answer.value?.payerName,
1457
+ ]
1458
+ }
1459
+ return []
1460
+ }, [responses])
1461
+
1462
+ const checkEligibility = useCallback(async () => {
1463
+ setLoading(true)
1464
+ setError(undefined)
1465
+
1466
+ try {
1467
+ // Step 1: Initiate eligibility check (creates patient → coverage → check)
1468
+ const { data } = await session.api.integrations.proxy_read({
1469
+ id: enduserId,
1470
+ integration: CANDID_TITLE,
1471
+ type: 'candid-eligibility',
1472
+ query: JSON.stringify({
1473
+ serviceCode: field.options?.candidServiceCode,
1474
+ npi: field.options?.candidNPI,
1475
+ payerId,
1476
+ memberId,
1477
+ payerName,
1478
+ }),
1479
+ })
1480
+
1481
+ const coverageId = data?.coverageId
1482
+ const checkId = data?.checkId
1483
+ const initialStatus = data?.status
1484
+
1485
+ if (!coverageId || !checkId) {
1486
+ throw new Error('No coverage ID or check ID returned from eligibility check')
1487
+ }
1488
+
1489
+ // If already completed, update answer immediately
1490
+ if (initialStatus === 'COMPLETED' || initialStatus === 'FAILED' || initialStatus === 'UNKNOWN') {
1491
+ onChange({
1492
+ payerId,
1493
+ status: initialStatus,
1494
+ coverageId,
1495
+ }, field.id)
1496
+ setLoading(false)
1497
+ return
1498
+ }
1499
+
1500
+ // Step 2: Poll for results
1501
+ setLoading(false)
1502
+ setPolling(true)
1503
+
1504
+ const maxAttempts = 60 // 2 minutes at 2s intervals
1505
+ let attempts = 0
1506
+
1507
+ const pollForResult = async (): Promise<void> => {
1508
+ if (attempts >= maxAttempts) {
1509
+ setError('Eligibility check timed out. Please try again.')
1510
+ setPolling(false)
1511
+ return
1512
+ }
1513
+
1514
+ attempts++
1515
+
1516
+ try {
1517
+ const { data: pollData } = await session.api.integrations.proxy_read({
1518
+ id: coverageId,
1519
+ integration: CANDID_TITLE,
1520
+ type: 'candid-eligibility-poll',
1521
+ query: JSON.stringify({ checkId }),
1522
+ })
1523
+
1524
+ const status = pollData?.status
1525
+ // Terminal statuses: COMPLETED, FAILED, or UNKNOWN (Candid returns UNKNOWN when eligibility cannot be determined)
1526
+ if (status === 'COMPLETED' || status === 'FAILED' || status === 'UNKNOWN') {
1527
+ onChange({
1528
+ payerId,
1529
+ status,
1530
+ coverageId,
1531
+ benefits: pollData?.benefits,
1532
+ }, field.id)
1533
+ setPolling(false)
1534
+ return
1535
+ }
1536
+
1537
+ // Still pending, poll again
1538
+ pollTimeoutRef.current = setTimeout(pollForResult, 2000)
1539
+ } catch (err: any) {
1540
+ setError(err?.message || 'Failed to check eligibility status')
1541
+ setPolling(false)
1542
+ }
1543
+ }
1544
+
1545
+ pollForResult()
1546
+ } catch (err: any) {
1547
+ setError(err?.message || 'Failed to check eligibility')
1548
+ console.error('Candid eligibility check failed:', err)
1549
+ setLoading(false)
1550
+ setPolling(false)
1551
+ }
1552
+ }, [session, field, payerId, memberId, payerName, onChange, enduserId])
1553
+
1554
+ // Auto-check eligibility for enduser sessions
1555
+ const autoCheckRef = useRef(false)
1556
+ useEffect(() => {
1557
+ if (!isEnduserSession) return
1558
+
1559
+ // If we already have a result and the payer hasn't changed, use the cached result
1560
+ if (value?.status && value?.payerId === payerId) {
1561
+ return
1562
+ }
1563
+
1564
+ if (autoCheckRef.current) return
1565
+ autoCheckRef.current = true
1566
+
1567
+ checkEligibility()
1568
+ }, [isEnduserSession, checkEligibility, value, payerId])
1569
+
1570
+ const errorComponent = useMemo(() => (
1571
+ <Grid container spacing={2} direction="column" alignItems="center" style={{ padding: '20px 0' }}>
1572
+ <Grid item>
1573
+ <Paper style={{
1574
+ padding: 16,
1575
+ backgroundColor: '#ffebee',
1576
+ border: '2px solid #f44336'
1577
+ }}>
1578
+ <Grid container spacing={2} direction="column" alignItems="center">
1579
+ <Grid item>
1580
+ <Typography variant="h2" style={{ color: '#f44336' }}>!</Typography>
1581
+ </Grid>
1582
+ <Grid item>
1583
+ <Typography variant="h6" align="center" color="error">
1584
+ Unable to Check Eligibility
1585
+ </Typography>
1586
+ </Grid>
1587
+ <Grid item>
1588
+ <Typography variant="body2" align="center" style={{ color: '#d32f2f' }}>
1589
+ {error}
1590
+ </Typography>
1591
+ </Grid>
1592
+ </Grid>
1593
+ </Paper>
1594
+ </Grid>
1595
+ </Grid>
1596
+ ), [error])
1597
+
1598
+ const checkingEligibilityComponent = useMemo(() => (
1599
+ <Grid container spacing={2} direction="column" alignItems="center" style={{ padding: '20px 0' }}>
1600
+ <Grid item>
1601
+ <CircularProgress size={40} />
1602
+ </Grid>
1603
+ <Grid item>
1604
+ <Typography variant="body1">
1605
+ {polling ? 'Verifying eligibility with insurance...' : 'Checking eligibility...'}
1606
+ </Typography>
1607
+ </Grid>
1608
+ <Grid item>
1609
+ <Typography variant="body2" color="textSecondary">
1610
+ {polling ? 'This usually takes 15-30 seconds' : 'This may take a few moments'}
1611
+ </Typography>
1612
+ </Grid>
1613
+ </Grid>
1614
+ ), [polling])
1615
+
1616
+ const resultsComponent = useMemo(() => {
1617
+ const isCompleted = value?.status === 'COMPLETED'
1618
+ const isFailed = value?.status === 'FAILED'
1619
+ return (
1620
+ <Grid container spacing={2} direction="column">
1621
+ <Grid item>
1622
+ <Paper style={{
1623
+ padding: 16,
1624
+ backgroundColor: isCompleted ? '#e8f5e9' : '#ffebee',
1625
+ border: `2px solid ${isCompleted ? '#4caf50' : '#f44336'}`
1626
+ }}>
1627
+ <Grid container spacing={2} direction="column" alignItems="center">
1628
+ <Grid item>
1629
+ {isCompleted ? (
1630
+ <CheckCircleOutline style={{ fontSize: 48, color: '#4caf50' }} />
1631
+ ) : (
1632
+ <Typography variant="h2" style={{ color: '#f44336' }}>!</Typography>
1633
+ )}
1634
+ </Grid>
1635
+ <Grid item>
1636
+ <Typography variant="h6" align="center">
1637
+ {isCompleted
1638
+ ? `${payerName || 'Insurance'} eligibility verified`
1639
+ : isFailed
1640
+ ? 'Eligibility check failed'
1641
+ : 'Eligibility Status: ' + first_letter_capitalized((value?.status || 'Unknown').toLowerCase())
1642
+ }
1643
+ </Typography>
1644
+ </Grid>
1645
+ </Grid>
1646
+ </Paper>
1647
+ </Grid>
1648
+ </Grid>
1649
+ )
1650
+ }, [value, payerName])
1651
+
1652
+ // Loading/polling state for enduser sessions
1653
+ if (isEnduserSession) {
1654
+ if (loading || polling) { return checkingEligibilityComponent }
1655
+ if (error) {
1656
+ return errorComponent
1657
+ }
1658
+ if (value?.status) {
1659
+ return resultsComponent
1660
+ }
1661
+ return errorComponent
1662
+ }
1663
+
1664
+ // User/admin interface (non-enduser sessions)
1665
+ return (
1666
+ <Grid container spacing={2} direction="column">
1667
+ <Grid item>
1668
+ <Typography variant="body2" color="textSecondary">
1669
+ Service Code: {field.options?.candidServiceCode || 'Not configured'}
1670
+ </Typography>
1671
+ <Typography variant="body2" color="textSecondary">
1672
+ Provider NPI: {field.options?.candidNPI || 'Not configured'}
1673
+ </Typography>
1674
+ {payerId && <Typography variant="body2" color="textSecondary">Payer ID: {payerId}</Typography>}
1675
+ {memberId && <Typography variant="body2" color="textSecondary">Member ID: {memberId}</Typography>}
1676
+ </Grid>
1677
+
1678
+ {error && (
1679
+ <Grid item>
1680
+ <Typography variant="body2" color="error">{error}</Typography>
1681
+ </Grid>
1682
+ )}
1683
+
1684
+ {polling && (
1685
+ <Grid item>
1686
+ <Typography variant="body2" color="primary">
1687
+ {form_display_text_for_language(form, "Polling for results... (this may take 15-30 seconds)")}
1688
+ </Typography>
1689
+ </Grid>
1690
+ )}
1691
+
1692
+ <Grid item container spacing={2}>
1693
+ <Grid item>
1694
+ <LoadingButton
1695
+ variant="outlined"
1696
+ onClick={checkEligibility}
1697
+ submitText={form_display_text_for_language(form, "Check Eligibility")}
1698
+ submittingText={polling ? form_display_text_for_language(form, "Polling...") : form_display_text_for_language(form, "Checking...")}
1699
+ submitting={loading || polling}
1700
+ disabled={loading || polling}
1701
+ />
1702
+ </Grid>
1703
+ </Grid>
1704
+
1705
+ {value && (
1706
+ <Grid item>
1707
+ <Typography variant="caption" color="textSecondary">
1708
+ Current Answer:
1709
+ </Typography>
1710
+ <pre style={{ fontSize: 11, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
1711
+ {JSON.stringify(value, null, 2)}
1712
+ </pre>
1713
+ </Grid>
1714
+ )}
1715
+ </Grid>
1716
+ )
1717
+ }
1718
+
1421
1719
  export const PharmacySearchInput = ({
1422
1720
  field,
1423
1721
  value: rawValue,