@tellescope/react-components 1.226.0 → 1.228.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellescope/react-components",
3
- "version": "1.226.0",
3
+ "version": "1.228.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.6.2",
48
48
  "@stripe/react-stripe-js": "^2.9.0",
49
49
  "@stripe/stripe-js": "^1.52.1",
50
- "@tellescope/constants": "^1.226.0",
51
- "@tellescope/sdk": "^1.226.0",
52
- "@tellescope/types-client": "^1.226.0",
53
- "@tellescope/types-models": "^1.226.0",
54
- "@tellescope/types-utilities": "^1.226.0",
55
- "@tellescope/utilities": "^1.226.0",
56
- "@tellescope/validation": "^1.226.0",
50
+ "@tellescope/constants": "^1.228.0",
51
+ "@tellescope/sdk": "^1.228.0",
52
+ "@tellescope/types-client": "^1.228.0",
53
+ "@tellescope/types-models": "^1.228.0",
54
+ "@tellescope/types-utilities": "^1.228.0",
55
+ "@tellescope/utilities": "^1.228.0",
56
+ "@tellescope/validation": "^1.228.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": "49646dd7f5488090911a4ffeddb5d771603de295",
86
+ "gitHead": "1bcfbd5c10a10553ce350df51799ef2bb1582f3d",
87
87
  "publishConfig": {
88
88
  "access": "public"
89
89
  }
@@ -1003,6 +1003,19 @@ export const useTellescopeForm = ({ dontAutoadvance, isPublicForm, form, urlLogi
1003
1003
  if (typeof value.answer.value?.inches !== 'number' || isNaN(value.answer.value?.inches)) {
1004
1004
  return "Inches must be provided (enter 0 for no inches)"
1005
1005
  }
1006
+
1007
+ // Convert height to total inches for min/max validation
1008
+ const totalInches = ((value.answer.value?.feet || 0) * 12) + (value.answer.value?.inches || 0)
1009
+ if (field.options?.min !== undefined && field.options.min !== -Infinity && totalInches < field.options.min) {
1010
+ const minFeet = Math.floor(field.options.min / 12)
1011
+ const minInches = field.options.min % 12
1012
+ return `Height must be at least ${minFeet}' ${minInches}"`
1013
+ }
1014
+ if (field.options?.max !== undefined && field.options.max !== Infinity && totalInches > field.options.max) {
1015
+ const maxFeet = Math.floor(field.options.max / 12)
1016
+ const maxInches = field.options.max % 12
1017
+ return `Height must be no more than ${maxFeet}' ${maxInches}"`
1018
+ }
1006
1019
  }
1007
1020
 
1008
1021
  if (value.answer.type === 'Related Contacts') {
@@ -1,11 +1,11 @@
1
1
  import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react"
2
2
  import axios from "axios"
3
- import { Autocomplete, Box, Button, Checkbox, Chip, Divider, FormControl, FormControlLabel, FormLabel, Grid, InputLabel, MenuItem, Radio, RadioGroup, Select, SxProps, TextField, TextFieldProps, Typography } from "@mui/material"
3
+ import { Autocomplete, Box, Button, Checkbox, Chip, Collapse, Divider, FormControl, FormControlLabel, FormLabel, Grid, IconButton as MuiIconButton, InputLabel, MenuItem, Radio, RadioGroup, Select, SxProps, TextField, TextFieldProps, Typography } from "@mui/material"
4
4
  import { FormInputProps } from "./types"
5
5
  import { useDropzone } from "react-dropzone"
6
6
  import { CANVAS_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, getLocalTimezone, getPublicFileURL, mm_dd_yyyy, replace_enduser_template_values, truncate_string, update_local_storage, user_display_name } from "@tellescope/utilities"
8
- import { Address, DatabaseSelectResponse, Enduser, EnduserRelationship, FormResponseValue, InsuranceRelationship, MedicationResponse, MultipleChoiceOptions, TellescopeGender, TIMEZONES_USA } from "@tellescope/types-models"
8
+ import { Address, DatabaseSelectResponse, Enduser, EnduserRelationship, FormResponseValue, InsuranceRelationship, MedicationResponse, MultipleChoiceOptions, FormFieldOptionDetails, TellescopeGender, TIMEZONES_USA } from "@tellescope/types-models"
9
9
  import { VALID_STATES, emailValidator, phoneValidator } from "@tellescope/validation"
10
10
  import Slider from '@mui/material/Slider';
11
11
  import LinearProgress from '@mui/material/LinearProgress';
@@ -23,7 +23,7 @@ import LanguageIcon from '@mui/icons-material/Language';
23
23
 
24
24
  import { Elements, PaymentElement, useStripe, useElements, EmbeddedCheckout, EmbeddedCheckoutProvider } from '@stripe/react-stripe-js';
25
25
  import { loadStripe } from '@stripe/stripe-js';
26
- import { CheckCircleOutline, Delete, Edit } from "@mui/icons-material"
26
+ import { CheckCircleOutline, Delete, Edit, ExpandMore } from "@mui/icons-material"
27
27
  import { WYSIWYG } from "./wysiwyg"
28
28
 
29
29
  export const LanguageSelect = ({ value, ...props }: { value: string, onChange: (s: string) => void}) => (
@@ -1504,12 +1504,24 @@ const multipleChoiceItemSx: SxProps = {
1504
1504
 
1505
1505
  export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: FormInputProps<'multiple_choice'>) => {
1506
1506
  const value = typeof _value === 'string' ? [_value] : _value // if loading existingResponses, allows them to be a string
1507
- const { choices, radio, other } = field.options as MultipleChoiceOptions
1507
+ const { choices, radio, other, optionDetails } = field.options as MultipleChoiceOptions
1508
+ const [expandedDescriptions, setExpandedDescriptions] = useState<Record<number, boolean>>({})
1508
1509
 
1509
1510
  // current other string
1510
1511
  const enteringOtherStringRef = React.useRef('') // if typing otherString as prefix of a checkbox value, don't auto-select
1511
1512
  const otherString = value?.find(v => v === enteringOtherStringRef.current || !(choices ?? [])?.find(c => c === v)) ?? ''
1512
1513
 
1514
+ const getDescriptionForChoice = useCallback((choice: string) => {
1515
+ return optionDetails?.find(detail => detail.option === choice)?.description
1516
+ }, [optionDetails])
1517
+
1518
+ const toggleDescription = useCallback((index: number) => {
1519
+ setExpandedDescriptions(prev => ({
1520
+ ...prev,
1521
+ [index]: !prev[index]
1522
+ }))
1523
+ }, [])
1524
+
1513
1525
  return (
1514
1526
  <Grid container alignItems="center">
1515
1527
  {radio
@@ -1523,48 +1535,134 @@ export const MultipleChoiceInput = ({ field, form, value: _value, onChange }: Fo
1523
1535
  defaultValue="female"
1524
1536
  name={`radio-group-${field.id}`}
1525
1537
  >
1526
- {(choices ?? []).map((c, i) =>
1527
- <FormControlLabel key={i} color="primary" label={c}
1528
- sx={multipleChoiceItemSx}
1529
- style={{ marginLeft: '0px' }} // fixes alignment with Select One text
1530
- checked={!!value?.includes(c) && c !== otherString} // coerce to boolean to keep as controlled input
1531
- control={
1532
- <Radio onClick={() => onChange(value?.includes(c) ? [] : [c], field.id)} />
1533
- }
1534
- />
1535
- )}
1538
+ {(choices ?? []).map((c, i) => {
1539
+ const description = getDescriptionForChoice(c)
1540
+ const hasDescription = !!description
1541
+ const isExpanded = expandedDescriptions[i]
1542
+
1543
+ return (
1544
+ <Box key={i} sx={{ width: '100%' }}>
1545
+ <Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
1546
+ <FormControlLabel
1547
+ sx={{ ...multipleChoiceItemSx, flex: 1, marginLeft: '0px' }}
1548
+ checked={!!value?.includes(c) && c !== otherString}
1549
+ control={
1550
+ <Radio onClick={() => onChange(value?.includes(c) ? [] : [c], field.id)} />
1551
+ }
1552
+ label={
1553
+ <Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
1554
+ <Typography component="span" sx={{ flex: 1 }}>{c}</Typography>
1555
+ {hasDescription && (
1556
+ <MuiIconButton
1557
+ size="small"
1558
+ onClick={(e: React.MouseEvent) => {
1559
+ e.stopPropagation()
1560
+ toggleDescription(i)
1561
+ }}
1562
+ sx={{
1563
+ transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
1564
+ transition: 'transform 0.2s',
1565
+ ml: 1
1566
+ }}
1567
+ >
1568
+ <ExpandMore fontSize="small" />
1569
+ </MuiIconButton>
1570
+ )}
1571
+ </Box>
1572
+ }
1573
+ />
1574
+ </Box>
1575
+ {hasDescription && (
1576
+ <Collapse in={isExpanded}>
1577
+ <Box sx={{ pl: '42px', pr: 2, pb: 1 }}>
1578
+ <Typography variant="body2" color="text.secondary">
1579
+ {description}
1580
+ </Typography>
1581
+ </Box>
1582
+ </Collapse>
1583
+ )}
1584
+ </Box>
1585
+ )
1586
+ })}
1536
1587
  </RadioGroup>
1537
1588
  </FormControl>
1538
1589
  ) : (
1539
- (choices ?? []).map((c, i) => (
1540
- <Grid xs={12} key={i}
1541
- onClick={() => onChange(
1542
- (
1543
- value?.includes(c)
1544
- ? (
1545
- (radio || field.options?.radioChoices?.includes(c))
1546
- ? []
1547
- : value.filter(v => v !== c)
1548
- )
1549
- : (
1550
- (radio || field.options?.radioChoices?.includes(c))
1551
- ? [c]
1552
- : [...(value ?? []).filter(x => !field.options?.radioChoices?.includes(x)), c]
1553
- )
1554
- ),
1555
- field.id,
1556
- )}
1557
- >
1558
- <Grid container alignItems="center" wrap="nowrap" sx={multipleChoiceItemSx}>
1559
- <Checkbox
1560
- color="primary"
1561
- checked={!!value?.includes(c) && c !== otherString} // coerce to boolean to keep as controlled input
1562
- inputProps={{ 'aria-label': 'primary checkbox' }}
1563
- />
1564
- <Typography component="span"> {c} </Typography>
1565
- </Grid>
1566
- </Grid>
1567
- ))
1590
+ (choices ?? []).map((c, i) => {
1591
+ const description = getDescriptionForChoice(c)
1592
+ const hasDescription = !!description
1593
+ const isExpanded = expandedDescriptions[i]
1594
+
1595
+ return (
1596
+ <Grid xs={12} key={i}>
1597
+ <Box sx={{ width: '100%' }}>
1598
+ <Box
1599
+ sx={{
1600
+ ...multipleChoiceItemSx,
1601
+ display: 'flex',
1602
+ alignItems: 'center',
1603
+ cursor: 'pointer',
1604
+ width: '100%'
1605
+ }}
1606
+ onClick={(e) => {
1607
+ // Don't trigger selection if clicking on the expand button
1608
+ if ((e.target as HTMLElement).closest('.expand-button')) {
1609
+ return
1610
+ }
1611
+ onChange(
1612
+ (
1613
+ value?.includes(c)
1614
+ ? (
1615
+ (radio || field.options?.radioChoices?.includes(c))
1616
+ ? []
1617
+ : value.filter(v => v !== c)
1618
+ )
1619
+ : (
1620
+ (radio || field.options?.radioChoices?.includes(c))
1621
+ ? [c]
1622
+ : [...(value ?? []).filter(x => !field.options?.radioChoices?.includes(x)), c]
1623
+ )
1624
+ ),
1625
+ field.id,
1626
+ )
1627
+ }}
1628
+ >
1629
+ <Checkbox
1630
+ color="primary"
1631
+ checked={!!value?.includes(c) && c !== otherString}
1632
+ inputProps={{ 'aria-label': 'primary checkbox' }}
1633
+ />
1634
+ <Typography component="span" sx={{ flex: 1 }}>{c}</Typography>
1635
+ {hasDescription && (
1636
+ <MuiIconButton
1637
+ className="expand-button"
1638
+ size="small"
1639
+ onClick={(e: React.MouseEvent) => {
1640
+ e.stopPropagation()
1641
+ toggleDescription(i)
1642
+ }}
1643
+ sx={{
1644
+ transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
1645
+ transition: 'transform 0.2s',
1646
+ ml: 1
1647
+ }}
1648
+ >
1649
+ <ExpandMore fontSize="small" />
1650
+ </MuiIconButton>
1651
+ )}
1652
+ </Box>
1653
+ {hasDescription && (
1654
+ <Collapse in={isExpanded}>
1655
+ <Box sx={{ pl: '42px', pr: 2, pb: 1 }}>
1656
+ <Typography variant="body2" color="text.secondary">
1657
+ {description}
1658
+ </Typography>
1659
+ </Box>
1660
+ </Collapse>
1661
+ )}
1662
+ </Box>
1663
+ </Grid>
1664
+ )
1665
+ })
1568
1666
  )
1569
1667
  }
1570
1668
  {other &&
@@ -1613,9 +1711,12 @@ export const StripeInput = ({ field, value, onChange, setCustomerId, enduserId }
1613
1711
  const [businessName, setBusinessName] = useState('')
1614
1712
  const [isCheckout, setIsCheckout] = useState(false)
1615
1713
  const [stripePromise, setStripePromise] = useState<ReturnType<typeof loadStripe>>()
1616
- const [, { findById: findProduct }] = useProducts({ dontFetch: true })
1617
1714
  const [answertext, setAnswertext] = useState('')
1618
1715
  const [error, setError] = useState('')
1716
+ const [selectedProducts, setSelectedProducts] = useState<string[]>([])
1717
+ const [showProductSelection, setShowProductSelection] = useState(false)
1718
+ const [availableProducts, setAvailableProducts] = useState<any[]>([])
1719
+ const [loadingProducts, setLoadingProducts] = useState(false)
1619
1720
 
1620
1721
  const fetchRef = useRef(false)
1621
1722
  useEffect(() => {
@@ -1623,6 +1724,35 @@ export const StripeInput = ({ field, value, onChange, setCustomerId, enduserId }
1623
1724
  if (value && (session.userInfo as any)?.stripeCustomerId) {
1624
1725
  return setCustomerId(c => c ? c : (session.userInfo as any)?.stripeCustomerId) // already paid or saved card
1625
1726
  }
1727
+
1728
+ // Check if product selection mode is enabled
1729
+ if (field.options?.stripeProductSelectionMode && (field.options?.productIds || []).length > 1) {
1730
+ setShowProductSelection(true)
1731
+ setLoadingProducts(true)
1732
+
1733
+ // Fetch product data with real-time Stripe pricing via proxy_read
1734
+ const productIds = (field.options.productIds || []).join(',')
1735
+ session.api.integrations.proxy_read({
1736
+ integration: 'Stripe',
1737
+ type: 'product-prices',
1738
+ id: productIds,
1739
+ query: field.options.stripeKey
1740
+ })
1741
+ .then(({ data }) => {
1742
+ setAvailableProducts(data.products || [])
1743
+ setLoadingProducts(false)
1744
+ })
1745
+ .catch((e: any) => {
1746
+ console.error('Error loading product data:', e)
1747
+ const errorMessage = e?.message?.includes?.('Stripe pricing error:')
1748
+ ? e.message.replace('Stripe pricing error: ', '')
1749
+ : 'Failed to load product information from Stripe'
1750
+ setError(`Product configuration error: ${errorMessage}`)
1751
+ setLoadingProducts(false)
1752
+ })
1753
+ return
1754
+ }
1755
+
1626
1756
  fetchRef.current = true
1627
1757
 
1628
1758
  session.api.form_responses.stripe_details({ fieldId: field.id, enduserId })
@@ -1643,10 +1773,156 @@ export const StripeInput = ({ field, value, onChange, setCustomerId, enduserId }
1643
1773
  }, [session, value, field.id, enduserId])
1644
1774
 
1645
1775
  const cost = (
1646
- (field.options?.productIds || []).map(id => findProduct(id, { batch: false })) // seems to be having issues with bulk read
1647
- .reduce((t, p) => t + (p?.cost?.amount || 0), 0)
1776
+ showProductSelection
1777
+ ? selectedProducts.reduce((total, productId) => {
1778
+ const product = availableProducts.find(p => p._id === productId)
1779
+ if (product?.currentPrice) {
1780
+ return total + (product.currentPrice.amount || 0)
1781
+ }
1782
+ return total + (product?.cost?.amount || 0)
1783
+ }, 0)
1784
+ : 0 // Will be calculated by existing Stripe flow when not in selection mode
1648
1785
  )
1649
1786
 
1787
+ // Handle product selection step
1788
+ if (showProductSelection) {
1789
+ if (error) {
1790
+ return (
1791
+ <Grid container direction="column" spacing={2} alignItems="center">
1792
+ <Grid item>
1793
+ <Typography color="error" variant="h6">
1794
+ Product Configuration Error
1795
+ </Typography>
1796
+ </Grid>
1797
+ <Grid item>
1798
+ <Typography color="error" sx={{ textAlign: 'center' }}>
1799
+ {error}
1800
+ </Typography>
1801
+ </Grid>
1802
+ </Grid>
1803
+ )
1804
+ }
1805
+
1806
+ if (loadingProducts) {
1807
+ return (
1808
+ <Grid container direction="column" spacing={2} alignItems="center">
1809
+ <Grid item>
1810
+ <LinearProgress />
1811
+ </Grid>
1812
+ <Grid item>
1813
+ <Typography>Loading product information...</Typography>
1814
+ </Grid>
1815
+ </Grid>
1816
+ )
1817
+ }
1818
+ const isSingleSelection = field.options?.radio === true
1819
+
1820
+ const handleProductSelection = (productId: string) => {
1821
+ if (isSingleSelection) {
1822
+ setSelectedProducts([productId])
1823
+ } else {
1824
+ setSelectedProducts(prev =>
1825
+ prev.includes(productId)
1826
+ ? prev.filter(id => id !== productId)
1827
+ : [...prev, productId]
1828
+ )
1829
+ }
1830
+ }
1831
+
1832
+ const handleContinueToPayment = () => {
1833
+ if (selectedProducts.length === 0) return
1834
+ setShowProductSelection(false)
1835
+ fetchRef.current = true
1836
+
1837
+ // Now fetch Stripe details with selected products
1838
+ session.api.form_responses.stripe_details({
1839
+ fieldId: field.id,
1840
+ enduserId,
1841
+ ...(selectedProducts.length > 0 && { selectedProductIds: selectedProducts }) // Pass selected products to Stripe checkout
1842
+ } as any)
1843
+ .then(({ clientSecret, publishableKey, stripeAccount, businessName, customerId, isCheckout, answerText }) => {
1844
+ setAnswertext(answerText || '')
1845
+ setIsCheckout(!!isCheckout)
1846
+ setClientSecret(clientSecret)
1847
+ setStripePromise(loadStripe(publishableKey, { stripeAccount }))
1848
+ setBusinessName(businessName)
1849
+ setCustomerId(customerId)
1850
+ })
1851
+ .catch((e: any) => {
1852
+ console.error(e)
1853
+ if (typeof e?.message === 'string') {
1854
+ setError(e.message)
1855
+ }
1856
+ })
1857
+ }
1858
+
1859
+ return (
1860
+ <Grid container direction="column" spacing={2}>
1861
+ <Grid item>
1862
+ <Typography variant="h6">Select Product{isSingleSelection ? '' : 's'}</Typography>
1863
+ </Grid>
1864
+
1865
+ {availableProducts.map((product) => {
1866
+ // Use real-time Stripe pricing if available, fallback to Tellescope pricing
1867
+ const price = product.currentPrice || product.cost
1868
+ const priceAmount = price?.amount || 0
1869
+ const priceCurrency = price?.currency || 'USD'
1870
+
1871
+ return (
1872
+ <Grid item key={product._id}>
1873
+ <FormControlLabel
1874
+ control={
1875
+ isSingleSelection ? (
1876
+ <Radio
1877
+ checked={selectedProducts.includes(product._id)}
1878
+ onChange={() => handleProductSelection(product._id)}
1879
+ />
1880
+ ) : (
1881
+ <Checkbox
1882
+ checked={selectedProducts.includes(product._id)}
1883
+ onChange={() => handleProductSelection(product._id)}
1884
+ />
1885
+ )
1886
+ }
1887
+ label={
1888
+ <Box>
1889
+ <Typography variant="body1" fontWeight="bold">
1890
+ {product.title}
1891
+ </Typography>
1892
+ {product.description && (
1893
+ <Typography variant="body2" color="textSecondary">
1894
+ {product.description}
1895
+ </Typography>
1896
+ )}
1897
+ <Typography variant="body2" color="primary">
1898
+ ${(priceAmount / 100).toFixed(2)} {priceCurrency.toUpperCase()}
1899
+ {product.currentPrice?.isSubscription && (
1900
+ <Typography component="span" variant="caption" sx={{ ml: 0.5 }}>
1901
+ /month
1902
+ </Typography>
1903
+ )}
1904
+ </Typography>
1905
+ </Box>
1906
+ }
1907
+ />
1908
+ </Grid>
1909
+ )
1910
+ })}
1911
+
1912
+ <Grid item>
1913
+ <Button
1914
+ variant="contained"
1915
+ onClick={handleContinueToPayment}
1916
+ disabled={selectedProducts.length === 0}
1917
+ sx={{ mt: 2 }}
1918
+ >
1919
+ Continue to Payment
1920
+ </Button>
1921
+ </Grid>
1922
+ </Grid>
1923
+ )
1924
+ }
1925
+
1650
1926
  if (error) {
1651
1927
  return (
1652
1928
  <Typography color="error">
@@ -1954,7 +2230,7 @@ export interface AddToDatabaseProps {
1954
2230
  onAdd: (record: DatabaseRecord) => void
1955
2231
  }
1956
2232
 
1957
- export const DatabaseSelectInput = ({ AddToDatabase, field, value: _value, onChange, onDatabaseSelect, responses, size, disabled }: FormInputProps<'Database Select'> & {
2233
+ export const DatabaseSelectInput = ({ AddToDatabase, field, value: _value, onChange, onDatabaseSelect, responses, size, disabled, enduser }: FormInputProps<'Database Select'> & {
1958
2234
  responses: FormResponseValue[],
1959
2235
  AddToDatabase?: React.JSXElementConstructor<AddToDatabaseProps>,
1960
2236
  }) => {
@@ -1988,6 +2264,23 @@ export const DatabaseSelectInput = ({ AddToDatabase, field, value: _value, onCha
1988
2264
  : undefined
1989
2265
  ), [responses, field.options?.databaseFilter])
1990
2266
 
2267
+ // State filtering logic similar to Insurance component
2268
+ const addressQuestion = useMemo(() => responses?.find(r => {
2269
+ if (r.answer.type !== 'Address') return false
2270
+ if (r.field.intakeField !== 'Address') return false
2271
+
2272
+ // make sure state is actually defined (in case of multiple address questions, where 1+ are blank)
2273
+ if (!r.answer.value?.state) return false
2274
+
2275
+ return true
2276
+ }), [responses])
2277
+
2278
+ const state = useMemo(() => (
2279
+ field.options?.filterByEnduserState
2280
+ ? ((addressQuestion?.answer?.type === 'Address' ? addressQuestion?.answer?.value?.state : undefined) || enduser?.state)
2281
+ : undefined
2282
+ ), [enduser?.state, addressQuestion, field.options?.filterByEnduserState])
2283
+
1991
2284
  const filteredChoicesWithPotentialDuplicates = useMemo(() => {
1992
2285
  if (!choices) return []
1993
2286
  if (!filterResponse) return choices
@@ -2030,17 +2323,29 @@ export const DatabaseSelectInput = ({ AddToDatabase, field, value: _value, onCha
2030
2323
  : false
2031
2324
  )
2032
2325
  }
2033
-
2326
+
2034
2327
  return false
2035
2328
  })
2036
2329
  )
2037
2330
  }, [choices, filterResponse, field.options?.databaseFilter, value])
2038
2331
 
2332
+ // Apply state filtering as a secondary filter (doesn't modify existing logic)
2333
+ const stateFilteredChoices = useMemo(() => {
2334
+ if (!field.options?.filterByEnduserState || !state) {
2335
+ return filteredChoicesWithPotentialDuplicates
2336
+ }
2337
+
2338
+ return filteredChoicesWithPotentialDuplicates.filter(c => {
2339
+ const recordState = c.values.find(v => v.label?.trim()?.toLowerCase() === 'state')?.value?.toString() || ''
2340
+ return !recordState || recordState === state
2341
+ })
2342
+ }, [filteredChoicesWithPotentialDuplicates, field.options?.filterByEnduserState, state])
2343
+
2039
2344
  const filteredChoices = useMemo(() => {
2040
- const filtered = []
2345
+ const filtered = []
2041
2346
 
2042
2347
  const uniques = new Set<string>([])
2043
- for (const c of filteredChoicesWithPotentialDuplicates) {
2348
+ for (const c of stateFilteredChoices) {
2044
2349
  const text = label_for_database_record(field, c)
2045
2350
  if (uniques.has(text)) continue // duplicate found
2046
2351
 
@@ -2049,7 +2354,7 @@ export const DatabaseSelectInput = ({ AddToDatabase, field, value: _value, onCha
2049
2354
  }
2050
2355
 
2051
2356
  return filtered
2052
- }, [field, filteredChoicesWithPotentialDuplicates])
2357
+ }, [field, stateFilteredChoices])
2053
2358
 
2054
2359
  if (!doneLoading) return <LinearProgress />
2055
2360
  return (