diff --git a/packages/sdk-react/src/components/approvalPolicies/ApprovalPolicyDetails/ApprovalPolicyForm/ApprovalPolicyForm.tsx b/packages/sdk-react/src/components/approvalPolicies/ApprovalPolicyDetails/ApprovalPolicyForm/ApprovalPolicyForm.tsx index 0320f08ed..f8a2e45c0 100644 --- a/packages/sdk-react/src/components/approvalPolicies/ApprovalPolicyDetails/ApprovalPolicyForm/ApprovalPolicyForm.tsx +++ b/packages/sdk-react/src/components/approvalPolicies/ApprovalPolicyDetails/ApprovalPolicyForm/ApprovalPolicyForm.tsx @@ -16,6 +16,10 @@ import { ApprovalPoliciesOperator, AmountTuple, } from '@/components/approvalPolicies/useApprovalPolicyTrigger'; +import { + CounterpartAutocomplete, + CounterpartsAutocompleteOptionProps, +} from '@/components/counterparts/CounterpartAutocomplete'; import { getCounterpartName } from '@/components/counterparts/helpers'; import { RHFTextField } from '@/components/RHF/RHFTextField'; import { useMoniteContext } from '@/core/context/MoniteContext'; @@ -45,10 +49,6 @@ import * as yup from 'yup'; import { ConditionsTable } from '../ConditionsTable'; import { RulesTable } from '../RulesTable'; -import { - AutocompleteCounterparts, - CounterpartsAutocompleteOptionProps, -} from './AutocompleteCounterparts'; import { AutocompleteTags } from './AutocompleteTags'; import { AutocompleteUsers } from './AutocompleteUsers'; @@ -919,10 +919,11 @@ export const ApprovalPolicyForm = ({ {(triggerInEdit === 'counterpart_id' || (isAddingTrigger && currentTriggerType === 'counterpart_id')) && ( - )} {(triggerInEdit === 'amount' || diff --git a/packages/sdk-react/src/components/approvalPolicies/ApprovalPolicyDetails/ApprovalPolicyForm/AutocompleteCounterparts/AutocompleteCounterparts.tsx b/packages/sdk-react/src/components/approvalPolicies/ApprovalPolicyDetails/ApprovalPolicyForm/AutocompleteCounterparts/AutocompleteCounterparts.tsx deleted file mode 100644 index 4b35324c3..000000000 --- a/packages/sdk-react/src/components/approvalPolicies/ApprovalPolicyDetails/ApprovalPolicyForm/AutocompleteCounterparts/AutocompleteCounterparts.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import { useState, useCallback, useMemo, useEffect } from 'react'; -import { Controller, Control, useFormContext } from 'react-hook-form'; - -import { getCounterpartName } from '@/components/counterparts/helpers'; -import { CreateCounterpartDialog } from '@/components/receivables/InvoiceDetails/CreateReceivable/sections/components/CreateCounterpartDialog'; -import { useMoniteContext } from '@/core/context/MoniteContext'; -import { useRootElements } from '@/core/context/RootElementsProvider'; -import { t } from '@lingui/macro'; -import { useLingui } from '@lingui/react'; -import AddIcon from '@mui/icons-material/Add'; -import { - Autocomplete, - CircularProgress, - TextField, - FormHelperText, - createFilterOptions, - Button, -} from '@mui/material'; - -import type { FormValues } from '../ApprovalPolicyForm'; - -interface AutocompleteCreatedByProps { - control: Control; - name: 'triggers.counterpart_id'; - label: string; -} - -export interface CounterpartsAutocompleteOptionProps { - id: string; - label: string; -} - -const COUNTERPART_CREATE_NEW_ID = '__create-new__'; - -export const AutocompleteCounterparts = ({ - control, - name, - label, -}: AutocompleteCreatedByProps) => { - const { i18n } = useLingui(); - const { api } = useMoniteContext(); - const { root } = useRootElements(); - const { setValue, getValues } = useFormContext(); - const [inputValue, setInputValue] = useState(''); - const [isCreateCounterpartOpened, setIsCreateCounterpartOpened] = - useState(false); - const [newCounterpartId, setNewCounterpartId] = useState(null); - - const handleCreateNewCounterpart = useCallback(() => { - setIsCreateCounterpartOpened(true); - }, [setIsCreateCounterpartOpened]); - - const handleCloseCreateCounterpart = useCallback(() => { - setIsCreateCounterpartOpened(false); - }, [setIsCreateCounterpartOpened]); - - const { - data: counterparts, - isLoading: isCounterpartsLoading, - refetch, - } = api.counterparts.getCounterparts.useQuery({ - query: { - ...(inputValue && { counterpart_name__icontains: inputValue }), - }, - }); - - const counterpartsAutocompleteData = useMemo< - Array - >( - () => - counterparts?.data.map((counterpart) => ({ - id: counterpart.id, - label: getCounterpartName(counterpart), - })) ?? [], - [counterparts] - ); - - const filter = createFilterOptions(); - - function isCreateNewCounterpartOption( - counterpartOption: CounterpartsAutocompleteOptionProps | undefined | null - ): boolean { - return counterpartOption?.id === COUNTERPART_CREATE_NEW_ID; - } - - useEffect(() => { - if (newCounterpartId) { - const currentValues = getValues(name) || []; - - const isAlreadyAdded = currentValues.some( - (counterpart: CounterpartsAutocompleteOptionProps) => - counterpart.id === newCounterpartId - ); - - if (!isAlreadyAdded) { - const existingCounterpart = counterpartsAutocompleteData.find( - (counterpart) => counterpart.id === newCounterpartId - ); - - const newCounterpart = { - id: newCounterpartId, - label: existingCounterpart - ? existingCounterpart.label - : 'New Counterpart', - }; - - setValue(name, [...currentValues, newCounterpart]); - } - } - }, [ - newCounterpartId, - setValue, - name, - getValues, - counterpartsAutocompleteData, - ]); - - return ( - <> - - ( - <> - - isCreateNewCounterpartOption(counterpartOption) - ? '' - : counterpartOption.label - } - getOptionKey={(option) => option.id} - isOptionEqualToValue={(option, value) => option.id === value.id} - filterOptions={(options, params) => { - const filtered = filter(options, params); - - filtered.unshift({ - id: COUNTERPART_CREATE_NEW_ID, - label: t(i18n)`Create new counterpart`, - }); - - return filtered; - }} - onInputChange={(_, newInputValue) => { - setInputValue(newInputValue); - }} - onChange={(_, value) => { - setValue(name, value); - refetch(); - }} - renderInput={(params) => ( - - {isCounterpartsLoading ? ( - - ) : null} - {params.InputProps.endAdornment} - - ), - }} - /> - )} - renderOption={(props, counterpartOption) => { - return isCreateNewCounterpartOption(counterpartOption) ? ( - - ) : ( -
  • - {counterpartOption.label} -
  • - ); - }} - /> - {error && {error.message}} - - )} - /> - - ); -}; diff --git a/packages/sdk-react/src/components/approvalPolicies/ApprovalPolicyDetails/ApprovalPolicyForm/AutocompleteCounterparts/index.ts b/packages/sdk-react/src/components/approvalPolicies/ApprovalPolicyDetails/ApprovalPolicyForm/AutocompleteCounterparts/index.ts deleted file mode 100644 index 5557ccfa3..000000000 --- a/packages/sdk-react/src/components/approvalPolicies/ApprovalPolicyDetails/ApprovalPolicyForm/AutocompleteCounterparts/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './AutocompleteCounterparts'; diff --git a/packages/sdk-react/src/components/counterparts/CounterpartAutocomplete/CounterpartAutocomplete.tsx b/packages/sdk-react/src/components/counterparts/CounterpartAutocomplete/CounterpartAutocomplete.tsx new file mode 100644 index 000000000..931c48c12 --- /dev/null +++ b/packages/sdk-react/src/components/counterparts/CounterpartAutocomplete/CounterpartAutocomplete.tsx @@ -0,0 +1,269 @@ +import { useState, useCallback, useMemo, useEffect } from 'react'; +import { + Controller, + Control, + useFormContext, + FieldValues, + FieldPath, + PathValue, +} from 'react-hook-form'; + +import type { + DefaultValuesOCRIndividual, + DefaultValuesOCROrganization, +} from '@/components/counterparts/Counterpart.types'; +import { CreateCounterpartDialog } from '@/components/counterparts/CreateCounterpartDialog'; +import { getCounterpartName } from '@/components/counterparts/helpers'; +import { useRootElements } from '@/core/context/RootElementsProvider'; +import { useCounterpartList } from '@/core/queries'; +import { t } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import AddIcon from '@mui/icons-material/Add'; +import { + Autocomplete, + CircularProgress, + TextField, + FormHelperText, + createFilterOptions, + Button, +} from '@mui/material'; + +export interface CounterpartsAutocompleteOptionProps { + id: string; + label: string; +} + +const COUNTERPART_CREATE_NEW_ID = '__create-new__'; + +const filter = createFilterOptions(); + +function isCreateNewCounterpartOption( + counterpartOption: CounterpartsAutocompleteOptionProps | undefined | null +): boolean { + return counterpartOption?.id === COUNTERPART_CREATE_NEW_ID; +} + +interface CounterpartAutocompleteProps { + control: Control; + name: FieldPath; + label: string; + disabled?: boolean; + required?: boolean; + getCounterpartDefaultValues?: ( + type?: string + ) => DefaultValuesOCRIndividual | DefaultValuesOCROrganization; + multiple?: boolean; +} + +export const CounterpartAutocomplete = ({ + control, + name, + label, + required = true, + getCounterpartDefaultValues, + multiple = false, + disabled = false, +}: CounterpartAutocompleteProps) => { + const { i18n } = useLingui(); + const { root } = useRootElements(); + const { setValue, getValues } = useFormContext(); + const [isCreateCounterpartOpened, setIsCreateCounterpartOpened] = + useState(false); + const [newCounterpartId, setNewCounterpartId] = useState(null); + console.log(newCounterpartId, 'newCounterpartId'); + const handleCreateNewCounterpart = useCallback(() => { + setIsCreateCounterpartOpened(true); + }, [setIsCreateCounterpartOpened]); + + const handleCloseCreateCounterpart = useCallback(() => { + setIsCreateCounterpartOpened(false); + }, [setIsCreateCounterpartOpened]); + + const { data: counterparts, isLoading: isCounterpartsLoading } = + useCounterpartList(); + + const counterpartsAutocompleteData = useMemo< + Array + >( + () => + counterparts?.data.map((counterpart) => ({ + id: counterpart.id, + label: getCounterpartName(counterpart), + })) ?? [], + [counterparts] + ); + + useEffect(() => { + if (newCounterpartId) { + const currentValues = getValues(name); + if (multiple) { + const values = currentValues ? currentValues : []; + const isAlreadyAdded = ( + values as Array + )?.some((counterpart) => counterpart.id === newCounterpartId); + + if (!isAlreadyAdded) { + const existingCounterpart = counterpartsAutocompleteData.find( + (counterpart) => counterpart.id === newCounterpartId + ); + + const newCounterpart = { + id: newCounterpartId, + label: existingCounterpart + ? existingCounterpart.label + : 'New Counterpart', + }; + + setValue(name, [ + ...(values as Array), + newCounterpart, + ] as PathValue>); + } + } else { + setValue( + name, + newCounterpartId as PathValue> + ); + } + } + }, [ + newCounterpartId, + setValue, + name, + getValues, + counterpartsAutocompleteData, + multiple, + ]); + + return ( + <> + + { + const selectedCounterpart = counterparts?.data.find( + (counterpart) => counterpart.id === field.value + ); + + /** + * We have to set `selectedCounterpartOption` to `null` + * if `selectedCounterpart` is `null` because + * `Autocomplete` component doesn't work with `undefined` + */ + const selectedCounterpartOption = selectedCounterpart + ? { + id: selectedCounterpart.id, + label: getCounterpartName(selectedCounterpart), + } + : null; + return ( + <> + + isCreateNewCounterpartOption(counterpartOption) + ? '' + : counterpartOption.label + } + getOptionKey={(option) => option.id} + isOptionEqualToValue={(option, value) => option.id === value.id} + filterOptions={(options, params) => { + const filtered = filter(options, params); + + filtered.unshift({ + id: COUNTERPART_CREATE_NEW_ID, + label: t(i18n)`Create new counterpart`, + }); + + return filtered; + }} + onChange={(_, value) => { + if (multiple) { + setValue( + name, + value as PathValue> + ); + } else { + setValue( + name, + (value as CounterpartsAutocompleteOptionProps | null) + ?.id as PathValue> + ); + } + }} + renderInput={(params) => ( + + {isCounterpartsLoading ? ( + + ) : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + renderOption={(props, counterpartOption) => { + return isCreateNewCounterpartOption(counterpartOption) ? ( + + ) : ( +
  • + {counterpartOption.label} +
  • + ); + }} + /> + {error && {error.message}} + + ); + }} + /> + + ); +}; diff --git a/packages/sdk-react/src/components/counterparts/CounterpartAutocomplete/index.tsx b/packages/sdk-react/src/components/counterparts/CounterpartAutocomplete/index.tsx new file mode 100644 index 000000000..055e61f8c --- /dev/null +++ b/packages/sdk-react/src/components/counterparts/CounterpartAutocomplete/index.tsx @@ -0,0 +1 @@ +export * from './CounterpartAutocomplete'; diff --git a/packages/sdk-react/src/components/receivables/InvoiceDetails/CreateReceivable/sections/components/CreateCounterpartDialog.tsx b/packages/sdk-react/src/components/counterparts/CreateCounterpartDialog/CreateCounterpartDialog.tsx similarity index 100% rename from packages/sdk-react/src/components/receivables/InvoiceDetails/CreateReceivable/sections/components/CreateCounterpartDialog.tsx rename to packages/sdk-react/src/components/counterparts/CreateCounterpartDialog/CreateCounterpartDialog.tsx diff --git a/packages/sdk-react/src/components/receivables/InvoiceDetails/CreateReceivable/sections/components/CreateCounterpartDialog.types.ts b/packages/sdk-react/src/components/counterparts/CreateCounterpartDialog/CreateCounterpartDialog.types.ts similarity index 100% rename from packages/sdk-react/src/components/receivables/InvoiceDetails/CreateReceivable/sections/components/CreateCounterpartDialog.types.ts rename to packages/sdk-react/src/components/counterparts/CreateCounterpartDialog/CreateCounterpartDialog.types.ts diff --git a/packages/sdk-react/src/components/counterparts/CreateCounterpartDialog/index.tsx b/packages/sdk-react/src/components/counterparts/CreateCounterpartDialog/index.tsx new file mode 100644 index 000000000..1f84ea8b2 --- /dev/null +++ b/packages/sdk-react/src/components/counterparts/CreateCounterpartDialog/index.tsx @@ -0,0 +1 @@ +export { CreateCounterpartDialog } from './CreateCounterpartDialog'; diff --git a/packages/sdk-react/src/components/counterparts/index.tsx b/packages/sdk-react/src/components/counterparts/index.tsx index ab754ac90..3f4defd3a 100644 --- a/packages/sdk-react/src/components/counterparts/index.tsx +++ b/packages/sdk-react/src/components/counterparts/index.tsx @@ -1,4 +1,5 @@ export * from './CounterpartsTable'; export * from './CounterpartDetails'; export * from './Counterparts'; +export * from './CounterpartAutocomplete'; export { getCounterpartName } from './helpers'; diff --git a/packages/sdk-react/src/components/payables/PayableDetails/PayableDetailsForm/PayableDetailsForm.tsx b/packages/sdk-react/src/components/payables/PayableDetails/PayableDetailsForm/PayableDetailsForm.tsx index 88a4d6cff..ecb512f52 100644 --- a/packages/sdk-react/src/components/payables/PayableDetails/PayableDetailsForm/PayableDetailsForm.tsx +++ b/packages/sdk-react/src/components/payables/PayableDetails/PayableDetailsForm/PayableDetailsForm.tsx @@ -13,7 +13,7 @@ import { DefaultValuesOCRIndividual, DefaultValuesOCROrganization, } from '@/components/counterparts/Counterpart.types'; -import { CounterpartAutocompleteWithCreate } from '@/components/receivables/InvoiceDetails/CreateReceivable/sections/components/CounterpartAutocompleteWithCreate'; +import { CounterpartAutocomplete } from '@/components/counterparts/CounterpartAutocomplete'; import { useMoniteContext } from '@/core/context/MoniteContext'; import { MoniteScopedProviders } from '@/core/context/MoniteScopedProviders'; import { useRootElements } from '@/core/context/RootElementsProvider'; @@ -286,7 +286,7 @@ const PayableDetailsFormBase = forwardRef< useEffect(() => { methods.reset(); - }, [payablesValidations, methods]); + }, [payablesValidations, methods.reset, methods]); useEffect(() => { reset(prepareDefaultValues(formatFromMinorUnits, payable, lineItems)); @@ -456,7 +456,8 @@ const PayableDetailsFormBase = forwardRef< /> )} /> - { return ( - (); - -const COUNTERPART_CREATE_NEW_ID = '__create-new__'; - -function isCreateNewCounterpartOption( - counterpartOption: CounterpartsAutocompleteOptionProps | undefined | null -): boolean { - return counterpartOption?.id === COUNTERPART_CREATE_NEW_ID; -} - -export const CounterpartAutocompleteWithCreate = < - TFieldValues extends FieldValues ->({ - disabled, - name, - label, - getCounterpartDefaultValues, - required = true, -}: { - disabled?: boolean; - name: FieldPath; - label: string; - getCounterpartDefaultValues?: ( - type?: string - ) => DefaultValuesOCRIndividual | DefaultValuesOCROrganization; - required?: boolean; -}) => { - const { i18n } = useLingui(); - const { control, setValue } = useFormContext(); - - const { root } = useRootElements(); - - const { data: counterparts, isLoading: isCounterpartsLoading } = - useCounterpartList(); - - const counterpartsAutocompleteData = useMemo< - Array - >( - () => - counterparts - ? counterparts?.data.map((counterpart) => ({ - id: counterpart.id, - label: getCounterpartName(counterpart), - })) - : [], - [counterparts] - ); - - const [isCreateCounterpartOpened, setIsCreateCounterpartOpened] = - useState(false); - - const handleCreateNewCounterpart = useCallback(() => { - setIsCreateCounterpartOpened(true); - }, [setIsCreateCounterpartOpened]); - - const [newCounterpartId, setNewCounterpartId] = useState(null); - - useEffect(() => { - if (newCounterpartId) { - setValue( - name, - newCounterpartId as PathValue> - ); - } - }, [newCounterpartId, setValue, name]); - - return ( - <> - { - setIsCreateCounterpartOpened(false); - }} - onCreate={setNewCounterpartId} - getCounterpartDefaultValues={getCounterpartDefaultValues} - /> - { - const selectedCounterpart = counterparts?.data.find( - (counterpart) => counterpart.id === field.value - ); - - /** - * We have to set `selectedCounterpartOption` to `null` - * if `selectedCounterpart` is `null` because - * `Autocomplete` component doesn't work with `undefined` - */ - const selectedCounterpartOption = selectedCounterpart - ? { - id: selectedCounterpart.id, - label: getCounterpartName(selectedCounterpart), - } - : null; - - return ( - { - if (isCreateNewCounterpartOption(value)) { - field.onChange(null); - - return; - } - - field.onChange(value?.id); - }} - slotProps={{ - popper: { - container: root, - }, - }} - filterOptions={(options, params) => { - const filtered = filter(options, params); - - filtered.unshift({ - id: COUNTERPART_CREATE_NEW_ID, - label: t(i18n)`Create new counterpart`, - }); - - return filtered; - }} - renderInput={(params) => ( - - ) : null, - }} - /> - )} - loading={isCounterpartsLoading || disabled} - options={counterpartsAutocompleteData} - getOptionLabel={(counterpartOption) => - isCreateNewCounterpartOption(counterpartOption) - ? '' - : counterpartOption.label - } - isOptionEqualToValue={(option, value) => option.id === value.id} - selectOnFocus - clearOnBlur - handleHomeEndKeys - renderOption={(props, counterpartOption) => - isCreateNewCounterpartOption(counterpartOption) ? ( - - ) : ( -
  • - {counterpartOption.label} -
  • - ) - } - /> - ); - }} - /> - - ); -};