diff --git a/example/src/Onboarding.tsx b/example/src/Onboarding.tsx index cb4eae92..be6da590 100644 --- a/example/src/Onboarding.tsx +++ b/example/src/Onboarding.tsx @@ -270,6 +270,19 @@ const OnboardingWithProps = ({ render={OnBoardingRender} employmentId={employmentId} externalId={externalId} + options={{ + jsfModify: { + basic_information: { + fields: { + tax_servicing_countries: { + presentation: { + enableCustomTaxServicingComponent: true, + }, + }, + }, + }, + }, + }} /> ); diff --git a/src/flows/Onboarding/components/TaxServicingCountries.tsx b/src/flows/Onboarding/components/TaxServicingCountries.tsx new file mode 100644 index 00000000..2afbd179 --- /dev/null +++ b/src/flows/Onboarding/components/TaxServicingCountries.tsx @@ -0,0 +1,218 @@ +import { useFormFields } from '@/src/context'; +import { $TSFixMe, Components, JSFField } from '@/src/types/remoteFlows'; +import { useFormContext } from 'react-hook-form'; +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/src/components/ui/form'; +import { MultiSelect } from '@/src/components/ui/multi-select'; +import { useState, useCallback, useMemo } from 'react'; + +type TaxCountriesFieldProps = JSFField & { + placeholder?: string; + options?: Array<{ value: string; label: string }>; + className?: string; + onChange?: (value: $TSFixMe) => void; + component?: Components['multi-select']; + $meta?: { + regions?: Record; + subregions?: Record; + }; +}; + +// Constants for tax servicing countries logic +const GLOBAL_OPTION = { label: 'Global', value: 'GLOBAL', type: 'global' }; +const AREA_TYPE = { + REGIONS: 'regions', + SUBREGIONS: 'subregions', + COUNTRIES: 'countries', +}; + +export function TaxServicingCountriesField({ + label, + name, + options, + defaultValue, + description, + onChange, + component, + $meta, + ...rest +}: TaxCountriesFieldProps) { + const { control } = useFormContext(); + const { components } = useFormFields(); + const [selected, setSelected] = useState<$TSFixMe[]>(); + + // Enhanced options with regions/subregions/countries + const enhancedOptions = useMemo(() => { + const { regions = {}, subregions = {} } = $meta || {}; + + const regionsOptions = [ + GLOBAL_OPTION, + ...Object.keys(regions).map((region) => ({ + label: region, + value: region, + type: AREA_TYPE.REGIONS, + category: 'Regions', + })), + ]; + + const subregionsOptions = Object.keys(subregions).map((subregion) => ({ + label: subregion, + value: subregion, + type: AREA_TYPE.SUBREGIONS, + category: 'Subregions', + })); + + const countriesOptions = + options?.map((option) => ({ + ...option, + type: AREA_TYPE.COUNTRIES, + category: 'Countries', + })) || []; + + return [...regionsOptions, ...subregionsOptions, ...countriesOptions]; + }, [options, $meta]); + + // Enhanced change handler with regions/subregions logic + const handleChange = useCallback( + (rawValues: $TSFixMe[]) => { + const { regions = {}, subregions = {} } = $meta || {}; + + if (!rawValues?.length) { + onChange?.([]); + setSelected([]); + return; + } + + // Check if Global is being selected + const isGlobalSelected = rawValues.some( + (option) => option.value === GLOBAL_OPTION.value, + ); + + // If Global is selected, clear all other selections and only keep Global + if (isGlobalSelected) { + // If Global was already selected and user is trying to add more options, ignore the new selections + if (selected?.some((option) => option.value === GLOBAL_OPTION.value)) { + return; // Don't change anything if Global is already selected + } + + // If Global is being selected for the first time, clear everything else + onChange?.([GLOBAL_OPTION.value]); + setSelected([GLOBAL_OPTION]); + return; + } + + // If Global is currently selected and user is trying to select other options, ignore them + if (selected?.some((option) => option.value === GLOBAL_OPTION.value)) { + return; // Don't allow other selections when Global is selected + } + + // Track countries to prevent duplicates when multiple overlapping areas are selected + const existingCountries = new Set(); + const updatedSelected = rawValues.flatMap((option) => { + // If the selection is a country, add it directly + if (option.type === AREA_TYPE.COUNTRIES) { + existingCountries.add(option.value); + return [option]; + } + + // For regions/subregions, look up their countries + const lookupLabel = option.originalLabel || option.label; + const areaCountries = + option.type === AREA_TYPE.REGIONS + ? regions[lookupLabel] || [] + : subregions[lookupLabel] || []; + + // Convert each country name into a country option object + return areaCountries + .filter((country) => !existingCountries.has(country)) + .map((country) => { + existingCountries.add(country); + return { + label: country, + value: country, + type: AREA_TYPE.COUNTRIES, + category: 'Countries', + }; + }); + }); + + // Extract just the country values for the form + const countryValues = updatedSelected + .filter((option) => option.type === AREA_TYPE.COUNTRIES) + .map((option) => option.value); + + onChange?.(countryValues); + setSelected(updatedSelected); + }, + [$meta, onChange, selected], // Added selected to dependencies + ); + + return ( + { + const CustomSelectField = component || components?.['multi-select']; + if (CustomSelectField) { + const customSelectFieldProps = { + label, + name, + options: enhancedOptions, + defaultValue, + description, + onChange: handleChange, + $meta, + ...rest, + }; + return ( + { + field.onChange(value); + handleChange(value); + }, + }} + fieldState={fieldState} + fieldData={customSelectFieldProps} + /> + ); + } + + const selectedOptions = + selected || + enhancedOptions.filter((option) => + field.value?.includes(option.value), + ); + + return ( + + + {label} + + + + + {description && {description}} + {fieldState.error && } + + ); + }} + /> + ); +} diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index ec1e0424..3a7f1a2f 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -39,6 +39,7 @@ import { FlowOptions, JSFModify, JSONSchemaFormType } from '@/src/flows/types'; import { AnnualGrossSalary } from '@/src/flows/Onboarding/components/AnnualGrossSalary'; import { $TSFixMe, JSFField, JSFFieldset, Meta } from '@/src/types/remoteFlows'; import { EquityPriceDetails } from '@/src/flows/Onboarding/components/EquityPriceDetails'; +import { TaxServicingCountriesField } from '@/src/flows/Onboarding/components/TaxServicingCountries'; type OnboardingHookProps = OnboardingFlowParams; @@ -277,13 +278,57 @@ export const useOnboarding = ({ Boolean(employmentId)), ); + const taxServicingCountriesField = + options?.jsfModify?.basic_information?.fields?.tax_servicing_countries; + const taxServicingCountriesFieldPresentation = + taxServicingCountriesField && + typeof taxServicingCountriesField === 'object' && + 'presentation' in taxServicingCountriesField + ? ( + taxServicingCountriesField as { + presentation?: { + enableCustomTaxServicingComponent?: boolean; + }; + } + ).presentation + : undefined; + + const customBasicInformationFields = useMemo(() => { + const enableCustomTaxServicingComponent = + taxServicingCountriesFieldPresentation?.enableCustomTaxServicingComponent !== + false; + + return { + fields: { + tax_servicing_countries: { + ...taxServicingCountriesField, + presentation: { + ...taxServicingCountriesFieldPresentation, + enableCustomTaxServicingComponent, + ...(enableCustomTaxServicingComponent && { + Component: (props: JSFField) => { + return ; + }, + }), + }, + }, + }, + }; + }, [taxServicingCountriesField, taxServicingCountriesFieldPresentation]); + const { data: basicInformationForm, isLoading: isLoadingBasicInformationForm, } = useJSONSchema({ form: 'employment_basic_information', options: { - jsfModify: options?.jsfModify?.basic_information, + jsfModify: { + ...options?.jsfModify?.basic_information, + fields: { + ...options?.jsfModify?.basic_information?.fields, + ...customBasicInformationFields.fields, + }, + }, queryOptions: { enabled: isBasicInformationDetailsEnabled, }, @@ -312,7 +357,7 @@ export const useOnboarding = ({ const equityCompensationField = options?.jsfModify?.contract_details?.fields?.equity_compensation; - const customFields = useMemo( + const customContractDetailsFields = useMemo( () => ({ fields: { annual_gross_salary: { @@ -380,7 +425,7 @@ export const useOnboarding = ({ ...options?.jsfModify?.contract_details, fields: { ...options?.jsfModify?.contract_details?.fields, - ...customFields.fields, + ...customContractDetailsFields.fields, }, }, queryOptions: {