@tellescope/react-components 1.238.0 → 1.239.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 (59) hide show
  1. package/lib/cjs/Forms/forms.d.ts.map +1 -1
  2. package/lib/cjs/Forms/forms.js +29 -27
  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 +29 -27
  6. package/lib/cjs/Forms/forms.v2.js.map +1 -1
  7. package/lib/cjs/Forms/hooks.d.ts +25 -1
  8. package/lib/cjs/Forms/hooks.d.ts.map +1 -1
  9. package/lib/cjs/Forms/hooks.js +40 -2
  10. package/lib/cjs/Forms/hooks.js.map +1 -1
  11. package/lib/cjs/Forms/inputs.d.ts +6 -2
  12. package/lib/cjs/Forms/inputs.d.ts.map +1 -1
  13. package/lib/cjs/Forms/inputs.js +109 -11
  14. package/lib/cjs/Forms/inputs.js.map +1 -1
  15. package/lib/cjs/Forms/inputs.v2.d.ts +2 -1
  16. package/lib/cjs/Forms/inputs.v2.d.ts.map +1 -1
  17. package/lib/cjs/Forms/inputs.v2.js +26 -9
  18. package/lib/cjs/Forms/inputs.v2.js.map +1 -1
  19. package/lib/cjs/Forms/localization.d.ts.map +1 -1
  20. package/lib/cjs/Forms/localization.js +7 -0
  21. package/lib/cjs/Forms/localization.js.map +1 -1
  22. package/lib/cjs/inputs_shared.d.ts +6 -2
  23. package/lib/cjs/inputs_shared.d.ts.map +1 -1
  24. package/lib/cjs/inputs_shared.js +4 -3
  25. package/lib/cjs/inputs_shared.js.map +1 -1
  26. package/lib/esm/Forms/forms.d.ts.map +1 -1
  27. package/lib/esm/Forms/forms.js +30 -28
  28. package/lib/esm/Forms/forms.js.map +1 -1
  29. package/lib/esm/Forms/forms.v2.d.ts.map +1 -1
  30. package/lib/esm/Forms/forms.v2.js +30 -28
  31. package/lib/esm/Forms/forms.v2.js.map +1 -1
  32. package/lib/esm/Forms/hooks.d.ts +25 -1
  33. package/lib/esm/Forms/hooks.d.ts.map +1 -1
  34. package/lib/esm/Forms/hooks.js +38 -1
  35. package/lib/esm/Forms/hooks.js.map +1 -1
  36. package/lib/esm/Forms/inputs.d.ts +6 -2
  37. package/lib/esm/Forms/inputs.d.ts.map +1 -1
  38. package/lib/esm/Forms/inputs.js +107 -10
  39. package/lib/esm/Forms/inputs.js.map +1 -1
  40. package/lib/esm/Forms/inputs.v2.d.ts +2 -1
  41. package/lib/esm/Forms/inputs.v2.d.ts.map +1 -1
  42. package/lib/esm/Forms/inputs.v2.js +25 -9
  43. package/lib/esm/Forms/inputs.v2.js.map +1 -1
  44. package/lib/esm/Forms/localization.d.ts.map +1 -1
  45. package/lib/esm/Forms/localization.js +7 -0
  46. package/lib/esm/Forms/localization.js.map +1 -1
  47. package/lib/esm/inputs_shared.d.ts +6 -2
  48. package/lib/esm/inputs_shared.d.ts.map +1 -1
  49. package/lib/esm/inputs_shared.js +4 -3
  50. package/lib/esm/inputs_shared.js.map +1 -1
  51. package/lib/tsconfig.tsbuildinfo +1 -1
  52. package/package.json +9 -9
  53. package/src/Forms/forms.tsx +9 -2
  54. package/src/Forms/forms.v2.tsx +9 -2
  55. package/src/Forms/hooks.tsx +57 -2
  56. package/src/Forms/inputs.tsx +223 -9
  57. package/src/Forms/inputs.v2.tsx +29 -11
  58. package/src/Forms/localization.ts +8 -0
  59. package/src/inputs_shared.tsx +6 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellescope/react-components",
3
- "version": "1.238.0",
3
+ "version": "1.239.0",
4
4
  "description": "",
5
5
  "main": "./lib/cjs/index.js",
6
6
  "module": "./lib/esm/index.js",
@@ -47,13 +47,13 @@
47
47
  "@reduxjs/toolkit": "1.9.0",
48
48
  "@stripe/react-stripe-js": "2.9.0",
49
49
  "@stripe/stripe-js": "1.52.1",
50
- "@tellescope/constants": "1.238.0",
51
- "@tellescope/sdk": "1.238.0",
52
- "@tellescope/types-client": "1.238.0",
53
- "@tellescope/types-models": "1.238.0",
54
- "@tellescope/types-utilities": "1.238.0",
55
- "@tellescope/utilities": "1.238.0",
56
- "@tellescope/validation": "1.238.0",
50
+ "@tellescope/constants": "1.239.0",
51
+ "@tellescope/sdk": "1.239.0",
52
+ "@tellescope/types-client": "1.239.0",
53
+ "@tellescope/types-models": "1.239.0",
54
+ "@tellescope/types-utilities": "1.239.0",
55
+ "@tellescope/utilities": "1.239.0",
56
+ "@tellescope/validation": "1.239.0",
57
57
  "@typescript-eslint/eslint-plugin": "4.33.0",
58
58
  "@typescript-eslint/parser": "4.33.0",
59
59
  "css-to-react-native": "3.0.0",
@@ -83,7 +83,7 @@
83
83
  "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
84
84
  "react-native": "^0.65.0 || ^0.66.0 || ^0.67.0 || ^0.68.0 || ^0.71.0"
85
85
  },
86
- "gitHead": "4986230d93e81703dbf27e595f8949645b35cdba",
86
+ "gitHead": "9a05a273aa2c4d1bb38d4252857586fd7732f678",
87
87
  "publishConfig": {
88
88
  "access": "public"
89
89
  }
@@ -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, 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, 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"
@@ -192,6 +192,7 @@ export const QuestionForField = ({
192
192
  const Conditions = customInputs?.['Conditions'] ?? ConditionsInput
193
193
  const RichText = customInputs?.['Rich Text'] ?? RichTextInput
194
194
  const BelugaPatientPreference = customInputs?.['Beluga Patient Preference'] ?? BelugaPatientPreferenceInput
195
+ const PharmacySearch = customInputs?.['Pharmacy Search'] ?? PharmacySearchInput
195
196
 
196
197
  const validationMessage = validateField(field)
197
198
 
@@ -344,7 +345,7 @@ export const QuestionForField = ({
344
345
  <Signature enduser={enduser} field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'signature'>} form={form}/>
345
346
  )
346
347
  : field.type === 'multiple_choice' ? (
347
- <MultipleChoice field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'multiple_choice'>} form={form}/>
348
+ <MultipleChoice field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'multiple_choice'>} form={form} responses={responses} enduser={enduser}/>
348
349
  )
349
350
  : field.type === 'Dropdown' ? (
350
351
  <Dropdown field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'Dropdown'>} form={form}/>
@@ -378,6 +379,12 @@ export const QuestionForField = ({
378
379
  onChange={onFieldChange as ChangeHandler<'Bridge Eligibility'>}
379
380
  />
380
381
  )
382
+ : field.type === 'Pharmacy Search' ? (
383
+ <PharmacySearch field={field} value={value.answer.value as any} form={form}
384
+ enduser={enduser} responses={responses}
385
+ onChange={onFieldChange as ChangeHandler<'Pharmacy Search'>}
386
+ />
387
+ )
381
388
  : field.type === 'rating' ? (
382
389
  <Rating field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'rating'>} form={form}/>
383
390
  )
@@ -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, 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, 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"
@@ -185,6 +185,7 @@ export const QuestionForField = ({
185
185
  const Conditions = customInputs?.['Conditions'] ?? ConditionsInput
186
186
  const RichText = customInputs?.['Rich Text'] ?? RichTextInput
187
187
  const BelugaPatientPreference = customInputs?.['Beluga Patient Preference'] ?? BelugaPatientPreferenceInput
188
+ const PharmacySearch = customInputs?.['Pharmacy Search'] ?? PharmacySearchInput
188
189
 
189
190
  const validationMessage = validateField(field)
190
191
 
@@ -338,7 +339,7 @@ export const QuestionForField = ({
338
339
  <Signature enduser={enduser} field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'signature'>} form={form}/>
339
340
  )
340
341
  : field.type === 'multiple_choice' ? (
341
- <MultipleChoice field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'multiple_choice'>} form={form}/>
342
+ <MultipleChoice field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'multiple_choice'>} form={form} responses={responses} enduser={enduser}/>
342
343
  )
343
344
  : field.type === 'Dropdown' ? (
344
345
  <Dropdown field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'Dropdown'>} form={form}/>
@@ -372,6 +373,12 @@ export const QuestionForField = ({
372
373
  onChange={onFieldChange as ChangeHandler<'Bridge Eligibility'>}
373
374
  />
374
375
  )
376
+ : field.type === 'Pharmacy Search' ? (
377
+ <PharmacySearch field={field} value={value.answer.value as any} form={form}
378
+ enduser={enduser} responses={responses}
379
+ onChange={onFieldChange as ChangeHandler<'Pharmacy Search'>}
380
+ />
381
+ )
375
382
  : field.type === 'rating' ? (
376
383
  <Rating field={field} value={value.answer.value as any} onChange={onFieldChange as ChangeHandler<'rating'>} form={form}/>
377
384
  )
@@ -4,7 +4,7 @@ import { ChangeHandler, FormFieldNode } from "./types"
4
4
  import { DatabaseRecord, Enduser, Form, FormField, FormResponse } from "@tellescope/types-client"
5
5
  import { phoneValidator } from "@tellescope/validation"
6
6
  import { FileBlob, Indexable } from "@tellescope/types-utilities"
7
- import { CompoundFilter, EnduserRelationship, FormCustomization, FormResponseAnswerAddress, FormResponseAnswerFileValue, FormResponseValue, FormResponseValueAnswer, OrganizationTheme, PreviousFormCompoundLogic, PreviousFormFieldType, Timezone, TIMEZONES } from "@tellescope/types-models"
7
+ import { CompoundFilter, EnduserRelationship, FormCustomization, FormFieldOptionDetails, FormResponseAnswerAddress, FormResponseAnswerFileValue, FormResponseValue, FormResponseValueAnswer, OrganizationTheme, PreviousFormCompoundLogic, PreviousFormFieldType, Timezone, TIMEZONES } from "@tellescope/types-models"
8
8
  import { WithTheme, contact_is_valid, useAddGTMTag, useFileUpload, useFormFields, useFormResponses, useResolvedSession, value_is_loaded } from "../index"
9
9
  import ReactGA from "react-ga4";
10
10
 
@@ -679,7 +679,7 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
679
679
  type: f.type,
680
680
  value: (
681
681
  existing_response_if_compatible(existingResponses, f) ?? (
682
- (f.type === 'Insurance' || f.type === 'Address' || f.type === 'file' || f.type === 'signature' || f.type === 'multiple_choice' || f.type === 'Dropdown' || f.type === 'Table Input' || f.type === 'Database Select' || f.type === 'Medications')
682
+ (f.type === 'Insurance' || f.type === 'Address' || f.type === 'file' || f.type === 'signature' || f.type === 'multiple_choice' || f.type === 'Dropdown' || f.type === 'Table Input' || f.type === 'Database Select' || f.type === 'Medications' || f.type === 'Pharmacy Search')
683
683
  ? undefined
684
684
  : f.type === 'Question Group'
685
685
  ? f.options?.subFields
@@ -1686,3 +1686,58 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1686
1686
  isAutoAdvancing,
1687
1687
  }
1688
1688
  }
1689
+
1690
+ /**
1691
+ * Hook for conditional visibility of multiple choice options.
1692
+ * Computes visible choices based on showCondition and provides an onChange wrapper
1693
+ * that auto-filters hidden choices from selections (event-driven, not useEffect).
1694
+ */
1695
+ export const useConditionalChoices = ({
1696
+ choices,
1697
+ optionDetails,
1698
+ responses,
1699
+ enduser,
1700
+ form,
1701
+ onChange,
1702
+ fieldId,
1703
+ otherString,
1704
+ }: {
1705
+ choices?: string[]
1706
+ optionDetails?: FormFieldOptionDetails[]
1707
+ responses?: Response[]
1708
+ enduser?: Partial<Enduser>
1709
+ form?: Form
1710
+ onChange: (value: string[], fieldId: string) => void
1711
+ fieldId: string
1712
+ otherString: string
1713
+ }) => {
1714
+ // Compute visible choices based on showCondition
1715
+ const visibleChoices = useMemo(() => {
1716
+ if (!choices) return []
1717
+ return choices.filter((choice, index) => {
1718
+ const optionDetail = optionDetails?.find(d => d.option === choice) ?? optionDetails?.[index]
1719
+ if (!optionDetail?.showCondition || object_is_empty(optionDetail.showCondition)) {
1720
+ return true
1721
+ }
1722
+ return responses_satisfy_conditions(responses || [], optionDetail.showCondition, {
1723
+ dateOfBirth: enduser?.dateOfBirth,
1724
+ gender: enduser?.gender,
1725
+ state: enduser?.state,
1726
+ form,
1727
+ activeResponses: responses,
1728
+ })
1729
+ })
1730
+ }, [choices, optionDetails, responses, enduser, form])
1731
+
1732
+ // Wrap onChange to auto-filter hidden choices (event-driven)
1733
+ const handleChange = useCallback((newValue: string[], fieldId: string) => {
1734
+ // Filter out any hidden choices from the new value
1735
+ // Allow through: visible choices, the "other" string, and values not in the choices array (custom "other" text)
1736
+ const filteredValue = newValue.filter(v =>
1737
+ visibleChoices.includes(v) || v === otherString || !choices?.includes(v)
1738
+ )
1739
+ onChange(filteredValue, fieldId)
1740
+ }, [visibleChoices, otherString, choices, onChange])
1741
+
1742
+ return { visibleChoices, handleChange }
1743
+ }
@@ -5,7 +5,7 @@ import { FormInputProps } from "./types"
5
5
  import { useDropzone } from "react-dropzone"
6
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
- import { Address, DatabaseSelectResponse, Enduser, EnduserRelationship, FormResponseValue, InsuranceRelationship, MedicationResponse, MultipleChoiceOptions, FormFieldOptionDetails, TellescopeGender, TIMEZONES_USA } from "@tellescope/types-models"
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"
10
10
  import Slider from '@mui/material/Slider';
11
11
  import LinearProgress from '@mui/material/LinearProgress';
@@ -25,6 +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
29
 
29
30
  // Bridge Eligibility - shared variable for storing most recent eligibility userIds
30
31
  const bridgeEligibilityResult = {
@@ -1417,6 +1418,207 @@ export const BridgeEligibilityInput = ({ field, value, onChange, responses, endu
1417
1418
  )
1418
1419
  }
1419
1420
 
1421
+ export const PharmacySearchInput = ({
1422
+ field,
1423
+ value: rawValue,
1424
+ onChange,
1425
+ responses,
1426
+ enduser,
1427
+ form,
1428
+ ...props
1429
+ }: Omit<FormInputProps<'string'>, 'value' | 'onChange'> & {
1430
+ value: Pharmacy | undefined,
1431
+ onChange: (v: Pharmacy | undefined, fieldId: string) => void
1432
+ }) => {
1433
+ const value = rawValue as Pharmacy | undefined
1434
+ const session = useResolvedSession()
1435
+
1436
+ // Get initial ZIP code from responses or enduser data
1437
+ const getInitialZipCode = () => {
1438
+ // Check Address field responses first
1439
+ const addressResponse = responses?.find(r =>
1440
+ r.answer?.type === 'Address' && (r.answer?.value as { zipCode?: string })?.zipCode
1441
+ )
1442
+ if (addressResponse?.answer?.type === 'Address') {
1443
+ const addressValue = addressResponse.answer.value as { zipCode?: string }
1444
+ if (addressValue?.zipCode) {
1445
+ return addressValue.zipCode
1446
+ }
1447
+ }
1448
+
1449
+ // Fall back to enduser.zipCode
1450
+ return enduser?.zipCode || ''
1451
+ }
1452
+
1453
+ const [zipCode, setZipCode] = useState(getInitialZipCode())
1454
+ const [loading, setLoading] = useState(false)
1455
+ const [error, setError] = useState<string>()
1456
+ const [pharmacies, setPharmacies] = useState<Pharmacy[]>([])
1457
+ const [hasSearched, setHasSearched] = useState(false)
1458
+
1459
+ const searchPharmacies = useCallback(async () => {
1460
+ if (!zipCode || zipCode.length !== 5) {
1461
+ setError(form_display_text_for_language(form, 'Please enter a valid 5-digit ZIP code'))
1462
+ return
1463
+ }
1464
+
1465
+ setLoading(true)
1466
+ setError(undefined)
1467
+ setHasSearched(true)
1468
+
1469
+ try {
1470
+ const { data } = await session.api.integrations.proxy_read({
1471
+ integration: 'ScriptSure',
1472
+ type: 'pharmacy-search',
1473
+ query: JSON.stringify({ zipCode }),
1474
+ })
1475
+
1476
+ setPharmacies(data || [])
1477
+
1478
+ if (!data?.length) {
1479
+ setError(form_display_text_for_language(form, 'No pharmacies found for this ZIP code'))
1480
+ }
1481
+ } catch (err: any) {
1482
+ setError(err?.message || form_display_text_for_language(form, 'Failed to search pharmacies'))
1483
+ setPharmacies([])
1484
+ } finally {
1485
+ setLoading(false)
1486
+ }
1487
+ }, [session, zipCode, form])
1488
+
1489
+ const handleSelectPharmacy = (pharmacy: Pharmacy) => {
1490
+ onChange({
1491
+ npi: pharmacy.npi,
1492
+ ncpdpId: pharmacy.ncpdpId,
1493
+ businessName: pharmacy.businessName,
1494
+ primaryTelephone: pharmacy.primaryTelephone || '',
1495
+ addressLine1: pharmacy.addressLine1,
1496
+ city: pharmacy.city,
1497
+ stateProvince: pharmacy.stateProvince,
1498
+ postalCode: pharmacy.postalCode,
1499
+ }, field.id)
1500
+ }
1501
+
1502
+ const formatPharmacyAddress = (p: Pharmacy) => {
1503
+ const parts = [p.addressLine1, p.city, p.stateProvince, p.postalCode].filter(Boolean)
1504
+ return parts.join(', ')
1505
+ }
1506
+
1507
+ return (
1508
+ <Grid container direction="column" spacing={2}>
1509
+ {/* ZIP Code Input */}
1510
+ <Grid item>
1511
+ <Grid container spacing={2} alignItems="flex-end">
1512
+ <Grid item xs={8} sm={6}>
1513
+ <TextField
1514
+ fullWidth
1515
+ size="small"
1516
+ label={form_display_text_for_language(form, "ZIP Code")}
1517
+ value={zipCode}
1518
+ onChange={(e) => setZipCode(e.target.value.replace(/\D/g, '').slice(0, 5))}
1519
+ InputProps={defaultInputProps}
1520
+ placeholder="12345"
1521
+ required={!field.isOptional}
1522
+ />
1523
+ </Grid>
1524
+ <Grid item xs={4} sm={6}>
1525
+ <LoadingButton
1526
+ variant="contained"
1527
+ onClick={searchPharmacies}
1528
+ submitText={form_display_text_for_language(form, "Search")}
1529
+ submittingText={form_display_text_for_language(form, "Searching...")}
1530
+ submitting={loading}
1531
+ disabled={zipCode.length !== 5 || loading}
1532
+ style={{ width: '100%', marginTop: 0 }}
1533
+ />
1534
+ </Grid>
1535
+ </Grid>
1536
+ </Grid>
1537
+
1538
+ {/* Error Message */}
1539
+ {error && (
1540
+ <Grid item>
1541
+ <Typography color="error" sx={{ fontSize: 14 }}>
1542
+ {error}
1543
+ </Typography>
1544
+ </Grid>
1545
+ )}
1546
+
1547
+ {/* Selected Pharmacy Display */}
1548
+ {value && (
1549
+ <Grid item>
1550
+ <Paper elevation={2} sx={{ p: 2, backgroundColor: '#e8f5e9' }}>
1551
+ <Grid container alignItems="center" justifyContent="space-between">
1552
+ <Grid item xs>
1553
+ <Typography variant="subtitle1" fontWeight="bold">
1554
+ <CheckCircleOutline sx={{ color: 'success.main', mr: 1, verticalAlign: 'middle' }} />
1555
+ {value.businessName}
1556
+ </Typography>
1557
+ <Typography variant="body2" color="text.secondary">
1558
+ {formatPharmacyAddress(value)}
1559
+ </Typography>
1560
+ {value.primaryTelephone && (
1561
+ <Typography variant="body2" color="text.secondary">
1562
+ {value.primaryTelephone}
1563
+ </Typography>
1564
+ )}
1565
+ </Grid>
1566
+ <Grid item>
1567
+ <MuiIconButton onClick={() => onChange(undefined as any, field.id)} size="small">
1568
+ <CancelIcon />
1569
+ </MuiIconButton>
1570
+ </Grid>
1571
+ </Grid>
1572
+ </Paper>
1573
+ </Grid>
1574
+ )}
1575
+
1576
+ {/* Search Results */}
1577
+ {!value && hasSearched && pharmacies.length > 0 && (
1578
+ <Grid item>
1579
+ <Typography variant="subtitle2" sx={{ mb: 1 }}>
1580
+ {pharmacies.length} {form_display_text_for_language(form, pharmacies.length === 1 ? 'pharmacy found' : 'pharmacies found')}
1581
+ </Typography>
1582
+ <Box sx={{ maxHeight: 300, overflow: 'auto' }}>
1583
+ {pharmacies.map((pharmacy, index) => (
1584
+ <Paper
1585
+ key={`${pharmacy.ncpdpId}-${index}`}
1586
+ elevation={1}
1587
+ sx={{
1588
+ p: 1.5,
1589
+ mb: 1,
1590
+ cursor: 'pointer',
1591
+ '&:hover': { backgroundColor: '#f5f5f5' },
1592
+ }}
1593
+ onClick={() => handleSelectPharmacy(pharmacy)}
1594
+ >
1595
+ <Typography variant="subtitle2" fontWeight="medium">
1596
+ {pharmacy.businessName}
1597
+ </Typography>
1598
+ <Typography variant="body2" color="text.secondary">
1599
+ {formatPharmacyAddress(pharmacy)}
1600
+ </Typography>
1601
+ {pharmacy.primaryTelephone && (
1602
+ <Typography variant="caption" color="text.secondary">
1603
+ {pharmacy.primaryTelephone}
1604
+ </Typography>
1605
+ )}
1606
+ </Paper>
1607
+ ))}
1608
+ </Box>
1609
+ </Grid>
1610
+ )}
1611
+
1612
+ {/* Loading State */}
1613
+ {loading && (
1614
+ <Grid item>
1615
+ <LinearProgress />
1616
+ </Grid>
1617
+ )}
1618
+ </Grid>
1619
+ )
1620
+ }
1621
+
1420
1622
  const HourSelector = (props : { value: string, onChange: (v: string) => void }) => (
1421
1623
  <StringSelector {...props}
1422
1624
  options={Array(12).fill('').map((_, i) => (i + 1) <= 9 ? `0${i + 1}` : (i + 1).toString())}
@@ -1998,7 +2200,7 @@ const multipleChoiceItemSx: SxProps = {
1998
2200
  },
1999
2201
  }
2000
2202
 
2001
- export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: FormInputProps<'multiple_choice'>) => {
2203
+ export const MultipleChoiceInput = ({ field, form, value: _value, onChange, responses, enduser }: FormInputProps<'multiple_choice'>) => {
2002
2204
  const value = typeof _value === 'string' ? [_value] : _value // if loading existingResponses, allows them to be a string
2003
2205
  const { choices, radio, other, optionDetails } = field.options as MultipleChoiceOptions
2004
2206
  const [expandedDescriptions, setExpandedDescriptions] = useState<Record<number, boolean>>({})
@@ -2007,6 +2209,18 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
2007
2209
  const enteringOtherStringRef = React.useRef('') // if typing otherString as prefix of a checkbox value, don't auto-select
2008
2210
  const otherString = value?.find(v => v === enteringOtherStringRef.current || !(choices ?? [])?.find(c => c === v)) ?? ''
2009
2211
 
2212
+ // Conditional visibility for choices
2213
+ const { visibleChoices, handleChange } = useConditionalChoices({
2214
+ choices,
2215
+ optionDetails,
2216
+ responses: responses as Response[] | undefined,
2217
+ enduser,
2218
+ form,
2219
+ onChange,
2220
+ fieldId: field.id,
2221
+ otherString,
2222
+ })
2223
+
2010
2224
  const getDescriptionForChoice = useCallback((choice: string) => {
2011
2225
  return optionDetails?.find(detail => detail.option === choice)?.description
2012
2226
  }, [optionDetails])
@@ -2031,7 +2245,7 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
2031
2245
  defaultValue="female"
2032
2246
  name={`radio-group-${field.id}`}
2033
2247
  >
2034
- {(choices ?? []).map((c, i) => {
2248
+ {visibleChoices.map((c, i) => {
2035
2249
  const description = getDescriptionForChoice(c)
2036
2250
  const hasDescription = !!description
2037
2251
  const isExpanded = expandedDescriptions[i]
@@ -2043,7 +2257,7 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
2043
2257
  sx={{ ...multipleChoiceItemSx, flex: 1, marginLeft: '0px' }}
2044
2258
  checked={!!value?.includes(c) && c !== otherString}
2045
2259
  control={
2046
- <Radio onClick={() => onChange(value?.includes(c) ? [] : [c], field.id)} />
2260
+ <Radio onClick={() => handleChange(value?.includes(c) ? [] : [c], field.id)} />
2047
2261
  }
2048
2262
  label={
2049
2263
  <Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
@@ -2083,7 +2297,7 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
2083
2297
  </RadioGroup>
2084
2298
  </FormControl>
2085
2299
  ) : (
2086
- (choices ?? []).map((c, i) => {
2300
+ visibleChoices.map((c, i) => {
2087
2301
  const description = getDescriptionForChoice(c)
2088
2302
  const hasDescription = !!description
2089
2303
  const isExpanded = expandedDescriptions[i]
@@ -2104,7 +2318,7 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
2104
2318
  if ((e.target as HTMLElement).closest('.expand-button')) {
2105
2319
  return
2106
2320
  }
2107
- onChange(
2321
+ handleChange(
2108
2322
  (
2109
2323
  value?.includes(c)
2110
2324
  ? (
@@ -2174,9 +2388,9 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
2174
2388
  // onClick={() => !otherChecked && handleOtherChecked()} // allow click to enable when disabled
2175
2389
  onChange={e => {
2176
2390
  enteringOtherStringRef.current = e.target.value
2177
- onChange(
2391
+ handleChange(
2178
2392
  (
2179
- radio
2393
+ radio
2180
2394
  ? (
2181
2395
  e.target.value.trim()
2182
2396
  ? [e.target.value]
@@ -2186,7 +2400,7 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
2186
2400
  e.target.value.trim()
2187
2401
  // remove existing other string (if exists) and append new one
2188
2402
  ? [...(value ?? []).filter(v => v !== otherString), e.target.value]
2189
- : value?.filter(v => v !== otherString)
2403
+ : (value ?? []).filter(v => v !== otherString)
2190
2404
  )
2191
2405
  ),
2192
2406
  field.id,
@@ -23,6 +23,7 @@ import LanguageIcon from '@mui/icons-material/Language';
23
23
 
24
24
  import { CheckCircleOutline, Delete, Edit, UploadFile } from "@mui/icons-material"
25
25
  import { WYSIWYG } from "./wysiwyg"
26
+ import { useConditionalChoices, Response } from "./hooks"
26
27
 
27
28
  export const LanguageSelect = ({ value, ...props }: { value: string, onChange: (s: string) => void}) => (
28
29
  <Grid container alignItems="center" justifyContent={"center"} wrap="nowrap" spacing={1}>
@@ -544,8 +545,8 @@ export const NumberInput = ({ field, value, onChange, form, ...props }: FormInpu
544
545
  )
545
546
  }
546
547
 
547
- // InsuranceInput, BridgeEligibilityInput, and AppointmentBookingInput logic is shared with inputs.tsx to avoid duplication
548
- import { InsuranceInput as SharedInsuranceInput, BridgeEligibilityInput as SharedBridgeEligibilityInput, AppointmentBookingInput as SharedAppointmentBookingInput } from './inputs'
548
+ // InsuranceInput, BridgeEligibilityInput, PharmacySearchInput, and AppointmentBookingInput logic is shared with inputs.tsx to avoid duplication
549
+ import { InsuranceInput as SharedInsuranceInput, BridgeEligibilityInput as SharedBridgeEligibilityInput, PharmacySearchInput as SharedPharmacySearchInput, AppointmentBookingInput as SharedAppointmentBookingInput } from './inputs'
549
550
 
550
551
  // Wrap the shared InsuranceInput component with v2-specific props
551
552
  export const InsuranceInput = (props: FormInputProps<'Insurance'>) => {
@@ -557,6 +558,11 @@ export const BridgeEligibilityInput = (props: FormInputProps<'Bridge Eligibility
557
558
  return <SharedBridgeEligibilityInput {...props} inputProps={defaultInputProps} />
558
559
  }
559
560
 
561
+ // Wrap the shared PharmacySearchInput component with v2-specific props
562
+ export const PharmacySearchInput = (props: FormInputProps<'Pharmacy Search'>) => {
563
+ return <SharedPharmacySearchInput {...props} />
564
+ }
565
+
560
566
 
561
567
  const StringSelector = ({ options, value, onChange, required, getDisplayValue, ...props } : {
562
568
  options: string[]
@@ -1154,7 +1160,7 @@ export const FilesInput = ({ value, onChange, field, existingFileName, uploading
1154
1160
  )
1155
1161
  }
1156
1162
 
1157
- export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: FormInputProps<'multiple_choice'>) => {
1163
+ export const MultipleChoiceInput = ({ field, form, value: _value, onChange, responses, enduser }: FormInputProps<'multiple_choice'>) => {
1158
1164
  const value = typeof _value === 'string' ? [_value] : _value // if loading existingResponses, allows them to be a string
1159
1165
  const { choices, radio, other, optionDetails } = field.options as MultipleChoiceOptions
1160
1166
 
@@ -1162,6 +1168,18 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1162
1168
  const enteringOtherStringRef = React.useRef('') // if typing otherString as prefix of a checkbox value, don't auto-select
1163
1169
  const otherString = value?.find(v => v === enteringOtherStringRef.current || !(choices ?? [])?.find(c => c === v)) ?? ''
1164
1170
 
1171
+ // Conditional visibility for choices
1172
+ const { visibleChoices, handleChange } = useConditionalChoices({
1173
+ choices,
1174
+ optionDetails,
1175
+ responses: responses as Response[] | undefined,
1176
+ enduser,
1177
+ form,
1178
+ onChange,
1179
+ fieldId: field.id,
1180
+ otherString,
1181
+ })
1182
+
1165
1183
  // Get primary color from form customization or use default
1166
1184
  const primaryColor = form?.customization?.primaryColor ?? '#798ED0'
1167
1185
 
@@ -1179,7 +1197,7 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1179
1197
  defaultValue="female"
1180
1198
  name={`radio-group-${field.id}`}
1181
1199
  >
1182
- {(choices ?? []).map((c, i) => {
1200
+ {visibleChoices.map((c, i) => {
1183
1201
  const description = getDescriptionForChoice(c)
1184
1202
  const hasDescription = !!description
1185
1203
  const isSelected = !!value?.includes(c) && c !== otherString
@@ -1203,7 +1221,7 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1203
1221
  backgroundColor: (theme: any) => `${theme.palette.primary.main}14`,
1204
1222
  },
1205
1223
  }}
1206
- onClick={() => onChange(value?.includes(c) ? [] : [c], field.id)}
1224
+ onClick={() => handleChange(value?.includes(c) ? [] : [c], field.id)}
1207
1225
  >
1208
1226
  <Typography component="span" sx={{ flex: 1, color: 'primary.main', fontSize: 13, fontWeight: 600 }}>{c}</Typography>
1209
1227
  </Box>
@@ -1216,11 +1234,11 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1216
1234
  )}
1217
1235
  </Box>
1218
1236
  )
1219
- })}
1237
+ })}
1220
1238
  </RadioGroup>
1221
1239
  </FormControl>
1222
1240
  ) : (
1223
- (choices ?? []).map((c, i) => {
1241
+ visibleChoices.map((c, i) => {
1224
1242
  const description = getDescriptionForChoice(c)
1225
1243
  const hasDescription = !!description
1226
1244
 
@@ -1236,7 +1254,7 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1236
1254
  boxSizing: 'border-box'
1237
1255
  }}
1238
1256
  onClick={(e) => {
1239
- onChange(
1257
+ handleChange(
1240
1258
  (
1241
1259
  value?.includes(c)
1242
1260
  ? (
@@ -1287,9 +1305,9 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1287
1305
  // onClick={() => !otherChecked && handleOtherChecked()} // allow click to enable when disabled
1288
1306
  onChange={e => {
1289
1307
  enteringOtherStringRef.current = e.target.value
1290
- onChange(
1308
+ handleChange(
1291
1309
  (
1292
- radio
1310
+ radio
1293
1311
  ? (
1294
1312
  e.target.value.trim()
1295
1313
  ? [e.target.value]
@@ -1299,7 +1317,7 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1299
1317
  e.target.value.trim()
1300
1318
  // remove existing other string (if exists) and append new one
1301
1319
  ? [...(value ?? []).filter(v => v !== otherString), e.target.value]
1302
- : value?.filter(v => v !== otherString)
1320
+ : (value ?? []).filter(v => v !== otherString)
1303
1321
  )
1304
1322
  ),
1305
1323
  field.id,
@@ -112,9 +112,17 @@ const SPANISH_TRANSLATIONS: Record<string, string> = {
112
112
 
113
113
  // Medications Search
114
114
  'Search': 'Buscar',
115
+ 'Searching...': 'Buscando...',
115
116
  'Drug Select': 'Seleccionar Medicamento',
116
117
  'Other Drug': 'Otro Medicamento',
117
118
 
119
+ // Pharmacy Search
120
+ 'pharmacies found': 'farmacias encontradas',
121
+ 'pharmacy found': 'farmacia encontrada',
122
+ 'Please enter a valid 5-digit ZIP code': 'Por favor ingrese un código postal válido de 5 dígitos',
123
+ 'No pharmacies found for this ZIP code': 'No se encontraron farmacias para este código postal',
124
+ 'Failed to search pharmacies': 'Error al buscar farmacias',
125
+
118
126
  // Height Input
119
127
  'Feet': 'Pies',
120
128
  'Inches': 'Pulgadas',