@tellescope/react-components 1.243.1 → 1.244.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.
- package/lib/cjs/Forms/forms.d.ts +1 -0
- package/lib/cjs/Forms/forms.d.ts.map +1 -1
- package/lib/cjs/Forms/forms.js +39 -37
- package/lib/cjs/Forms/forms.js.map +1 -1
- package/lib/cjs/Forms/forms.v2.d.ts +1 -0
- package/lib/cjs/Forms/forms.v2.d.ts.map +1 -1
- package/lib/cjs/Forms/forms.v2.js +47 -40
- package/lib/cjs/Forms/forms.v2.js.map +1 -1
- package/lib/cjs/Forms/hooks.d.ts +1 -0
- package/lib/cjs/Forms/hooks.d.ts.map +1 -1
- package/lib/cjs/Forms/hooks.js +71 -28
- package/lib/cjs/Forms/hooks.js.map +1 -1
- package/lib/cjs/Forms/inputs.d.ts +5 -0
- package/lib/cjs/Forms/inputs.d.ts.map +1 -1
- package/lib/cjs/Forms/inputs.js +202 -12
- package/lib/cjs/Forms/inputs.js.map +1 -1
- package/lib/cjs/Forms/inputs.v2.d.ts +1 -0
- package/lib/cjs/Forms/inputs.v2.d.ts.map +1 -1
- package/lib/cjs/Forms/inputs.v2.js +16 -6
- package/lib/cjs/Forms/inputs.v2.js.map +1 -1
- package/lib/cjs/forms.d.ts +2 -1
- package/lib/cjs/forms.d.ts.map +1 -1
- package/lib/cjs/forms.js +2 -2
- package/lib/cjs/forms.js.map +1 -1
- package/lib/cjs/inputs_shared.d.ts +1 -0
- package/lib/cjs/inputs_shared.d.ts.map +1 -1
- package/lib/cjs/inputs_shared.js +27 -3
- package/lib/cjs/inputs_shared.js.map +1 -1
- package/lib/esm/Forms/forms.d.ts +1 -0
- package/lib/esm/Forms/forms.d.ts.map +1 -1
- package/lib/esm/Forms/forms.js +40 -38
- package/lib/esm/Forms/forms.js.map +1 -1
- package/lib/esm/Forms/forms.v2.d.ts +1 -0
- package/lib/esm/Forms/forms.v2.d.ts.map +1 -1
- package/lib/esm/Forms/forms.v2.js +48 -41
- package/lib/esm/Forms/forms.v2.js.map +1 -1
- package/lib/esm/Forms/hooks.d.ts +1 -0
- package/lib/esm/Forms/hooks.d.ts.map +1 -1
- package/lib/esm/Forms/hooks.js +70 -28
- package/lib/esm/Forms/hooks.js.map +1 -1
- package/lib/esm/Forms/inputs.d.ts +5 -0
- package/lib/esm/Forms/inputs.d.ts.map +1 -1
- package/lib/esm/Forms/inputs.js +202 -13
- package/lib/esm/Forms/inputs.js.map +1 -1
- package/lib/esm/Forms/inputs.v2.d.ts +1 -0
- package/lib/esm/Forms/inputs.v2.d.ts.map +1 -1
- package/lib/esm/Forms/inputs.v2.js +16 -7
- package/lib/esm/Forms/inputs.v2.js.map +1 -1
- package/lib/esm/forms.d.ts +2 -1
- package/lib/esm/forms.d.ts.map +1 -1
- package/lib/esm/forms.js +2 -2
- package/lib/esm/forms.js.map +1 -1
- package/lib/esm/inputs_shared.d.ts +1 -0
- package/lib/esm/inputs_shared.d.ts.map +1 -1
- package/lib/esm/inputs_shared.js +28 -4
- package/lib/esm/inputs_shared.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/Forms/forms.tsx +10 -2
- package/src/Forms/forms.v2.tsx +29 -3
- package/src/Forms/hooks.tsx +46 -1
- package/src/Forms/inputs.tsx +304 -6
- package/src/Forms/inputs.v2.tsx +19 -4
- package/src/forms.tsx +3 -1
- 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.
|
|
3
|
+
"version": "1.244.1",
|
|
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.
|
|
55
|
-
"@tellescope/sdk": "1.
|
|
56
|
-
"@tellescope/types-client": "1.
|
|
57
|
-
"@tellescope/types-models": "1.
|
|
58
|
-
"@tellescope/types-utilities": "1.
|
|
59
|
-
"@tellescope/utilities": "1.
|
|
60
|
-
"@tellescope/validation": "1.
|
|
54
|
+
"@tellescope/constants": "1.244.1",
|
|
55
|
+
"@tellescope/sdk": "1.244.1",
|
|
56
|
+
"@tellescope/types-client": "1.244.1",
|
|
57
|
+
"@tellescope/types-models": "1.244.1",
|
|
58
|
+
"@tellescope/types-utilities": "1.244.1",
|
|
59
|
+
"@tellescope/utilities": "1.244.1",
|
|
60
|
+
"@tellescope/validation": "1.244.1",
|
|
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": "
|
|
87
|
+
"gitHead": "a650ed3f130600bb7a191723532154011b30b6df",
|
|
88
88
|
"publishConfig": {
|
|
89
89
|
"access": "public"
|
|
90
90
|
}
|
package/src/Forms/forms.tsx
CHANGED
|
@@ -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}
|
package/src/Forms/forms.v2.tsx
CHANGED
|
@@ -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}
|
package/src/Forms/hooks.tsx
CHANGED
|
@@ -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
|
}
|
package/src/Forms/inputs.tsx
CHANGED
|
@@ -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,
|