diff --git a/src/locale/it.ts b/src/locale/it.ts index bf4bd9c4..24f46eaf 100644 --- a/src/locale/it.ts +++ b/src/locale/it.ts @@ -231,6 +231,13 @@ export default { adminTooltip: 'Per aggiungere questo ruolo è richiesta la sottoscrizione di un modulo da parte del Legale Rappresentante', }, + addOnAggregatedEntities: { + title: "Seleziona l'ambito di operatività", + radioTitle1: 'Solo per il mio ente', + radioDescription1: "Potrà gestire e operare solo all'interno del proprio ente", + radioTitle2: 'Per tutti gli enti aggregati', + radioDescription2: 'Potrà gestire e operare su tutti gli enti', + }, addLegalRepresentative: { title: 'Indica il Legale Rappresentante', subTitle: diff --git a/src/microcomponents/mock_dashboard/data/party.ts b/src/microcomponents/mock_dashboard/data/party.ts index 5161d6b6..55295297 100644 --- a/src/microcomponents/mock_dashboard/data/party.ts +++ b/src/microcomponents/mock_dashboard/data/party.ts @@ -317,6 +317,7 @@ export const mockedParties: Array = [ institutionType: 'PA', origin: 'IPA', originId: 'originId1', + isAggregator: true, }, { productId: 'prod-pn', diff --git a/src/pages/addUserFlow/addUser/AddProductToUserPage.tsx b/src/pages/addUserFlow/addUser/AddProductToUserPage.tsx index ec36d253..6c0f5fbb 100644 --- a/src/pages/addUserFlow/addUser/AddProductToUserPage.tsx +++ b/src/pages/addUserFlow/addUser/AddProductToUserPage.tsx @@ -11,7 +11,7 @@ import { Product } from '../../../model/Product'; import { ProductsRolesMap } from '../../../model/ProductRole'; import { RequestOutcomeMessage } from '../../../model/UserRegistry'; import AddLegalRepresentativeForm from '../addManager/AddLegalRepresentativeForm'; -import AddUserForm from './components/AddUserForm'; +import AddUserForm from './components/AddUserForm/AddUserForm'; import { MessageNoAction } from './components/MessageNoAction'; const CustomTextTransform = styled(Typography)({ diff --git a/src/pages/addUserFlow/addUser/AddUsersPage.tsx b/src/pages/addUserFlow/addUser/AddUsersPage.tsx index 51a96fc2..3e4489f0 100644 --- a/src/pages/addUserFlow/addUser/AddUsersPage.tsx +++ b/src/pages/addUserFlow/addUser/AddUsersPage.tsx @@ -18,7 +18,7 @@ import { getUserCountService } from '../../../services/usersService'; import { LOADING_TASK_GET_USER_ADMIN_COUNT, PRODUCT_IDS } from '../../../utils/constants'; import { ENV } from '../../../utils/env'; import AddLegalRepresentativeForm from '../addManager/AddLegalRepresentativeForm'; -import AddUserForm from './components/AddUserForm'; +import AddUserForm from './components/AddUserForm/AddUserForm'; import { MessageNoAction } from './components/MessageNoAction'; type Props = { diff --git a/src/pages/addUserFlow/addUser/__tests__/AddUserForm.test.tsx b/src/pages/addUserFlow/addUser/__tests__/AddUserForm.test.tsx index 2521787d..93c2e1ba 100644 --- a/src/pages/addUserFlow/addUser/__tests__/AddUserForm.test.tsx +++ b/src/pages/addUserFlow/addUser/__tests__/AddUserForm.test.tsx @@ -1,10 +1,13 @@ import { fireEvent, screen, waitFor } from '@testing-library/react'; import { RoleEnum } from '../../../../api/generated/onboarding/UserDto'; import { mockedParties } from '../../../../microcomponents/mock_dashboard/data/party'; -import { mockedPartyProducts, mockedProductRoles } from '../../../../microcomponents/mock_dashboard/data/product'; +import { + mockedPartyProducts, + mockedProductRoles, +} from '../../../../microcomponents/mock_dashboard/data/product'; import { productRoles2ProductRolesList } from '../../../../model/ProductRole'; import { renderWithProviders } from '../../../../utils/test-utils'; -import AddUserForm from '../components/AddUserForm'; +import AddUserForm from '../components/AddUserForm/AddUserForm'; describe('AddUserForm Component', () => { const defaultProps = { @@ -190,4 +193,3 @@ describe('AddUserForm Component', () => { expect(screen.getByRole('button', { name: /Continua/i })).toBeDisabled(); }); }); - diff --git a/src/pages/addUserFlow/addUser/components/AddUserForm.tsx b/src/pages/addUserFlow/addUser/components/AddUserForm.tsx deleted file mode 100644 index 9d39ff4c..00000000 --- a/src/pages/addUserFlow/addUser/components/AddUserForm.tsx +++ /dev/null @@ -1,748 +0,0 @@ -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; -import { - Box, - Button, - Checkbox, - Divider, - FormControl, - FormControlLabel, - Grid, - InputLabel, - MenuItem, - OutlinedInput, - Radio, - Select, - Stack, - styled, - Tooltip, - Typography, -} from '@mui/material'; -import { ButtonNaked, theme } from '@pagopa/mui-italia'; -import { TitleBox, usePermissions } from '@pagopa/selfcare-common-frontend/lib'; -import useErrorDispatcher from '@pagopa/selfcare-common-frontend/lib/hooks/useErrorDispatcher'; -import useLoading from '@pagopa/selfcare-common-frontend/lib/hooks/useLoading'; -import { - useUnloadEventInterceptor, - useUnloadEventOnExit, -} from '@pagopa/selfcare-common-frontend/lib/hooks/useUnloadEventInterceptor'; -import useUserNotify from '@pagopa/selfcare-common-frontend/lib/hooks/useUserNotify'; -import { Actions, emailRegexp } from '@pagopa/selfcare-common-frontend/lib/utils/constants'; -import { resolvePathVariables } from '@pagopa/selfcare-common-frontend/lib/utils/routes-utils'; -import { verifyChecksumMatchWithTaxCode } from '@pagopa/selfcare-common-frontend/lib/utils/verifyChecksumMatchWithTaxCode'; -import { verifyNameMatchWithTaxCode } from '@pagopa/selfcare-common-frontend/lib/utils/verifyNameMatchWithTaxCode'; -import { verifySurnameMatchWithTaxCode } from '@pagopa/selfcare-common-frontend/lib/utils/verifySurnameMatchWithTaxCode'; -import { useFormik } from 'formik'; -import { Dispatch, SetStateAction, useEffect, useState } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; -import { useHistory } from 'react-router'; -import { useIsMobile } from '../../../../hooks/useIsMobile'; -import { Party } from '../../../../model/Party'; -import { AddedUsersList, PartyUserOnCreation, TextTransform } from '../../../../model/PartyUser'; -import { Product } from '../../../../model/Product'; -import { ProductRole, ProductRolesLists, ProductsRolesMap } from '../../../../model/ProductRole'; -import { UserRegistry } from '../../../../model/UserRegistry'; -import { DASHBOARD_USERS_ROUTES } from '../../../../routes'; -import { fetchUserRegistryByFiscalCode } from '../../../../services/usersService'; -import { - LOADING_TASK_FETCH_TAX_CODE, - LOADING_TASK_SAVE_PARTY_USER, -} from '../../../../utils/constants'; -import { commonStyles, CustomTextField, getProductLink, renderLabel } from '../../utils/helpers'; -import { requiredError, taxCodeRegexp } from '../../utils/validation'; -import { useCheckOnboardedUser } from '../hooks/useCheckOnboardedUser'; -import { useSaveUser } from '../hooks/useSaveUser'; - -const CustomFormControlLabel = styled(FormControlLabel)({ - disabled: false, - '.MuiRadio-root': { - color: '#0073E6', - }, -}); - -type Props = { - party: Party; - userId?: string; - selectedProduct?: Product; - products: Array; - productsRolesMap: ProductsRolesMap; - canEditRegistryData: boolean; - initialFormData: PartyUserOnCreation; - goBack?: () => void; - forwardNextStep: () => void; - handlePreviousStep?: () => void; - setCurrentSelectedProduct: Dispatch>; - setAddedUserList: Dispatch>>; - isAddInBulkEAFlow: boolean; - setIsAddInBulkEAFlow: Dispatch>; -}; - -// eslint-disable-next-line sonarjs/cognitive-complexity -export default function AddUserForm({ - party, - userId, - selectedProduct, - products, - productsRolesMap, - canEditRegistryData, - initialFormData, - goBack, - forwardNextStep, - setCurrentSelectedProduct, - setAddedUserList, - isAddInBulkEAFlow, - setIsAddInBulkEAFlow, -}: Readonly) { - const { t } = useTranslation(); - const setLoadingSaveUser = useLoading(LOADING_TASK_SAVE_PARTY_USER); - const setLoadingFetchTaxCode = useLoading(LOADING_TASK_FETCH_TAX_CODE); - - const addError = useErrorDispatcher(); - const addNotify = useUserNotify(); - const isMobile = useIsMobile('lg'); - - const history = useHistory(); - - const [validTaxcode, setValidTaxcode] = useState(); - const [userProduct, setUserProduct] = useState(); - const [productRoles, setProductRoles] = useState(); - const [productInPage, setProductInPage] = useState(); - const [isAsyncFlow, setIsAsyncFlow] = useState(false); - const [dynamicDocLink, setDynamicDocLink] = useState(''); - - const { registerUnloadEvent, unregisterUnloadEvent } = useUnloadEventInterceptor(); - const { hasPermission } = usePermissions(); - const onExit = useUnloadEventOnExit(); - - const isPnpgTheOnlyProduct = - !!products.find((p) => p.id === 'prod-pn-pg') && products.length === 1; - const pnpgProduct = products.find((p) => p.id === 'prod-pn-pg'); - - const activeOnboardings = party.products.filter((p) => p.productOnBoardingStatus === 'ACTIVE'); - - useEffect(() => { - if (!initialFormData.taxCode) { - if (validTaxcode && validTaxcode !== initialFormData.taxCode) { - fetchTaxCode(validTaxcode, party.partyId); - } else if ( - !validTaxcode && - formik.values.certifiedName === true && - formik.values.certifiedSurname === true - ) { - void formik.setValues( - { - ...formik.values, - name: formik.initialValues.name, - surname: formik.initialValues.surname, - email: formik.initialValues.email, - confirmEmail: '', - certifiedName: formik.initialValues.certifiedName, - certifiedSurname: formik.initialValues.certifiedSurname, - }, - true - ); - } - } - }, [validTaxcode]); - - useEffect(() => { - if (initialFormData.taxCode) { - setValidTaxcode(initialFormData.taxCode); - } - }, [initialFormData]); - - useEffect(() => { - setUserProduct(selectedProduct); - }, [selectedProduct]); - - useEffect(() => { - if (isPnpgTheOnlyProduct && initialFormData.taxCode === '') { - setUserProduct(pnpgProduct); - } - }, []); - - useEffect(() => { - if (userProduct) { - setCurrentSelectedProduct(userProduct); - - const matchingProduct = activeOnboardings.find((p) => p.productId === userProduct.id); - - const institutionType = matchingProduct?.institutionType ?? party.institutionType; - - setDynamicDocLink(getProductLink(userProduct.id, institutionType)); - } - }, [userProduct, activeOnboardings, party.institutionType]); - - const goBackInner = - goBack ?? - (() => - history.push( - resolvePathVariables( - selectedProduct - ? DASHBOARD_USERS_ROUTES.PARTY_PRODUCT_USERS.path - : DASHBOARD_USERS_ROUTES.PARTY_USERS.path, - { - partyId: party.partyId, - productId: userProduct?.id ?? '', - } - ) - )); - - useEffect(() => { - const isEnabled = products.filter((p) => - party.products.some( - (pp) => - p.id === pp.productId && - hasPermission(pp.productId || '', Actions.CreateProductUsers) && - pp.productOnBoardingStatus === 'ACTIVE' - ) - ); - - setProductInPage(Object.keys(isEnabled).length === 1); - if (productInPage) { - setUserProduct(isEnabled[0]); - } - }, [productInPage]); - - const saveUser = useSaveUser({ - party, - userProduct, - productRoles, - t, - addNotify, - addError, - history, - setLoadingSaveUser, - unregisterUnloadEvent, - initialFormData, - selectedProduct, - isPnpgTheOnlyProduct, - }); - - const checkOnboardedUser = useCheckOnboardedUser({ - partyId: party.partyId, - userProductId: userProduct?.id, - t, - addError, - addNotify, - forwardNextStep, - setAddedUserList, - isAddInBulkEAFlow, - isAsyncFlow, - productRoles, - }); - - const errorNotify = (errors: any, taxCode: string) => - addError({ - id: 'FETCH_TAX_CODE', - blocking: false, - error: errors, - techDescription: `An error occurred while fetching Fiscal Code of Product ${taxCode}`, - toNotify: true, - }); - - const buildFormValues = (userRegistry: UserRegistry | null) => { - void formik.setValues({ - ...formik.values, - name: - userRegistry?.name ?? - (formik.values.certifiedName ? initialFormData.name : formik.values.name), - surname: - userRegistry?.surname ?? - (formik.values.certifiedSurname ? initialFormData.surname : formik.values.surname), - email: - userRegistry?.email ?? - (formik.values.certifiedName || formik.values.certifiedSurname - ? initialFormData.email - : formik.values.email), - confirmEmail: '', - certifiedName: - userRegistry?.certifiedName ?? - (formik.values.certifiedName ? initialFormData.certifiedName : formik.values.certifiedName), - certifiedSurname: - userRegistry?.certifiedSurname ?? - (formik.values.certifiedSurname - ? initialFormData.certifiedSurname - : formik.values.certifiedSurname), - }); - }; - - const fetchTaxCode = (taxCode: string, partyId: string) => { - setLoadingFetchTaxCode(true); - fetchUserRegistryByFiscalCode(taxCode.toUpperCase(), partyId) - .then((userRegistry) => { - buildFormValues(userRegistry); - }) - .catch((errors) => errorNotify(errors, taxCode)) - .finally(() => setLoadingFetchTaxCode(false)); - }; - - // eslint-disable-next-line sonarjs/cognitive-complexity - const validate = (values: Partial) => { - const errors = Object.fromEntries( - Object.entries({ - name: !values.name - ? requiredError - : verifyNameMatchWithTaxCode(values.name, values.taxCode) - ? t('userEdit.mismatchWithTaxCode.name') - : undefined, - surname: !values.surname - ? requiredError - : verifySurnameMatchWithTaxCode(values.surname, values.taxCode) - ? t('userEdit.mismatchWithTaxCode.surname') - : undefined, - taxCode: !values.taxCode - ? requiredError - : !taxCodeRegexp.test(values.taxCode) || verifyChecksumMatchWithTaxCode(values.taxCode) - ? t('userEdit.addForm.errors.invalidFiscalCode') - : undefined, - email: !values.email - ? requiredError - : !emailRegexp.test(values.email) - ? t('userEdit.addForm.errors.invalidEmail') - : undefined, - confirmEmail: !values.confirmEmail - ? requiredError - : values.email && - values.confirmEmail.toLocaleLowerCase() !== values.email.toLocaleLowerCase() - ? t('userEdit.addForm.errors.mismatchEmail') - : undefined, - productRoles: values.productRoles?.length === 0 ? requiredError : undefined, - }).filter(([_key, value]) => value) - ); - if (!errors.taxCode) { - setValidTaxcode(values.taxCode); - } else { - setValidTaxcode(undefined); - } - return errors; - }; - - const addOneRoleModal = (values: PartyUserOnCreation) => { - addNotify({ - component: 'SessionModal', - id: 'ONE_ROLE_USER', - title: t('userEdit.addForm.addOneRoleModal.title'), - message: ( - productRoles?.groupByProductRole[r].title)}`, - productTitle: `${userProduct?.title}`, - }} - components={{ - 1: , - 3: , - 5: , - }} - > - {`Vuoi assegnare a <1>{{user}} il ruolo di <3>{{role}} per <5>{{productTitle}}?<7><8><9>`} - - ), - onConfirm: () => - saveUser( - { - ...values, - taxCode: values.taxCode.toUpperCase(), - email: values.email.toLowerCase(), - }, - userId - ), - confirmLabel: t('userEdit.addForm.addOneRoleModal.confirmButton'), - closeLabel: t('userEdit.addForm.addOneRoleModal.closeButton'), - }); - }; - - const addMultiRoleModal = (values: PartyUserOnCreation) => { - addNotify({ - component: 'SessionModal', - id: 'MULTI_ROLE_USER', - title: t('userEdit.addForm.addMultiRoleModal.title'), - message: ( - productRoles?.groupByProductRole[r].title) - .join(', ')}`, - productTitle: `${userProduct?.title}.`, - }} - components={{ 1: , 3: , 5: }} - > - {`Stai per assegnare a <1>{{user}} i ruoli <3>{{roles}} sul prodotto <5>{{productTitle}}<6><7><8>Confermi di voler continuare?<9>`} - - ), - // eslint-disable-next-line sonarjs/no-identical-functions - onConfirm: () => - saveUser( - { - ...values, - taxCode: values.taxCode.toUpperCase(), - email: values.email.toLowerCase(), - }, - userId - ), - confirmLabel: t('userEdit.addForm.addMultiRoleModal.confirmButton'), - closeLabel: t('userEdit.addForm.addMultiRoleModal.closeButton'), - }); - }; - - const formik = useFormik({ - initialValues: initialFormData, - validate, - onSubmit: (values: PartyUserOnCreation) => { - if (isAsyncFlow || isAddInBulkEAFlow) { - checkOnboardedUser(values); - return; - } - - if (values.productRoles.length >= 2) { - addMultiRoleModal(values); - } else { - addOneRoleModal(values); - } - }, - }); - - useEffect(() => { - if (formik.dirty || userProduct) { - registerUnloadEvent(); - } else { - unregisterUnloadEvent(); - } - }, [formik.dirty, userProduct]); - - useEffect(() => { - if (userProduct) { - setProductRoles(productsRolesMap[userProduct.id]); - void formik.setFieldValue('productRoles', [], true); - } - }, [userProduct]); - - const addRole = (r: ProductRole) => { - // eslint-disable-next-line functional/no-let - let nextProductRoles; - if (r.multiroleAllowed && formik.values.productRoles.length > 0) { - if (productRoles?.groupByProductRole[formik.values.productRoles[0]].selcRole !== r.selcRole) { - nextProductRoles = [r.productRole]; - } else { - const productRoleIndex = formik.values.productRoles.findIndex((p) => p === r.productRole); - if (productRoleIndex === -1) { - nextProductRoles = formik.values.productRoles.concat([r.productRole]); - } else { - nextProductRoles = formik.values.productRoles.filter((_p, i) => i !== productRoleIndex); - } - } - } else { - nextProductRoles = [r.productRole]; - } - void formik.setFieldValue('productRoles', nextProductRoles, true); - }; - - const baseTextFieldProps = ( - field: keyof PartyUserOnCreation, - label: string, - placeholder: string, - textTransform?: TextTransform - ) => { - const isError = !!formik.errors[field] && formik.errors[field] !== requiredError; - - return { - id: field, - type: 'text', - value: formik.values[field], - label, - placeholder, - error: isError, - helperText: isError ? formik.errors[field] : undefined, - required: true, - variant: 'outlined' as const, - onChange: formik.handleChange, - sx: { width: '100%' }, - InputProps: { - style: { - fontSize: 'fontSize', - fontWeight: 'fontWeightMedium', - lineHeight: '24px', - color: 'text.primary', - textAlign: 'start' as const, - }, - }, - inputProps: { - style: { - textTransform, - }, - }, - }; - }; - - const selectLabel = t('userEdit.addForm.product.selectLabel'); - - const isAddRoleFromDashboard = (phasesAdditionAllowed?: Array) => - !!phasesAdditionAllowed && phasesAdditionAllowed[0].startsWith('dashboard'); - - const isAddRoleFromDashboardAsync = (phasesAdditionAllowed?: Array) => - !!phasesAdditionAllowed && phasesAdditionAllowed[0] === 'dashboard-async'; - - return ( -
- {canEditRegistryData ? ( - - - - - - - - - - - - - - - - - - - ) : undefined} - - {!selectedProduct && !isPnpgTheOnlyProduct ? ( - - - - - - - - {selectLabel} - - - - - - ) : undefined} - - {productRoles && ( - - - {dynamicDocLink.length > 0 && ( - - { - window.open(dynamicDocLink); - }} - > - {t('userEdit.addForm.role.documentationLink')} - - - )} - {Object.values(productRoles.groupBySelcRole).map((roles) => - roles - .filter((r) => isAddRoleFromDashboard(r.phasesAdditionAllowed)) - .map((p, index: number, filteredRoles) => ( - <> - - -1} - disabled={!validTaxcode} - value={p.productRole} - control={roles.length > 1 && p.multiroleAllowed ? : } - label={renderLabel(p, !!validTaxcode)} - aria-label={`${p.title}`} - onClick={ - validTaxcode - ? () => { - addRole(p); - setIsAddInBulkEAFlow( - p?.phasesAdditionAllowed.includes('dashboard-aggregator') && - party.products.some( - (p) => p.productId === userProduct?.id && p.isAggregator - ) - ); - setIsAsyncFlow(p?.phasesAdditionAllowed.includes('dashboard-async')); - } - : undefined - } - /> - {isAddRoleFromDashboardAsync(p?.phasesAdditionAllowed) && ( - - - - )} - - {filteredRoles.length !== index && ( - - - - )} - - )) - )} - - )} - - - - - -
- ); -} diff --git a/src/pages/addUserFlow/addUser/components/AddUserForm/AddUserForm.tsx b/src/pages/addUserFlow/addUser/components/AddUserForm/AddUserForm.tsx new file mode 100644 index 00000000..c5f70ee8 --- /dev/null +++ b/src/pages/addUserFlow/addUser/components/AddUserForm/AddUserForm.tsx @@ -0,0 +1,493 @@ +import { Button, Grid, Radio, RadioGroup, Stack } from '@mui/material'; +import { TitleBox, usePermissions } from '@pagopa/selfcare-common-frontend/lib'; +import useErrorDispatcher from '@pagopa/selfcare-common-frontend/lib/hooks/useErrorDispatcher'; +import useLoading from '@pagopa/selfcare-common-frontend/lib/hooks/useLoading'; +import { + useUnloadEventInterceptor, + useUnloadEventOnExit, +} from '@pagopa/selfcare-common-frontend/lib/hooks/useUnloadEventInterceptor'; +import useUserNotify from '@pagopa/selfcare-common-frontend/lib/hooks/useUserNotify'; +import { Actions } from '@pagopa/selfcare-common-frontend/lib/utils/constants'; +import { resolvePathVariables } from '@pagopa/selfcare-common-frontend/lib/utils/routes-utils'; +import { useFormik } from 'formik'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { useHistory } from 'react-router'; +import { useIsMobile } from '../../../../../hooks/useIsMobile'; +import { Party } from '../../../../../model/Party'; +import { AddedUsersList, PartyUserOnCreation, TextTransform } from '../../../../../model/PartyUser'; +import { Product } from '../../../../../model/Product'; +import { ProductRole, ProductRolesLists, ProductsRolesMap } from '../../../../../model/ProductRole'; +import { DASHBOARD_USERS_ROUTES } from '../../../../../routes'; +import { fetchUserRegistryByFiscalCode } from '../../../../../services/usersService'; +import { + LOADING_TASK_FETCH_TAX_CODE, + LOADING_TASK_SAVE_PARTY_USER, + PRODUCT_IDS, +} from '../../../../../utils/constants'; +import { + commonStyles, + CustomFormControlLabel, + getProductLink, + RadioOptionLabel, + renderLabel, +} from '../../../utils/helpers'; +import { requiredError } from '../../../utils/validation'; +import { useCheckOnboardedUser } from '../../hooks/useCheckOnboardedUser'; +import { useSaveUser } from '../../hooks/useSaveUser'; +import { ProductRolesSection } from './components/ProductRolesSection'; +import { ProductSelectionSection } from './components/ProductSelectionSection'; +import { UserDataSection } from './components/UserDataSection'; +import { useAddUserFormComputedValues, useFormValidation } from './hooks/useAddUserFormLogic'; +import { buildFormValues, EA_RADIO_OPTIONS } from './utils/addUserFormUtils'; + +type Props = { + party: Party; + userId?: string; + selectedProduct?: Product; + products: Array; + productsRolesMap: ProductsRolesMap; + canEditRegistryData: boolean; + initialFormData: PartyUserOnCreation; + goBack?: () => void; + forwardNextStep: () => void; + handlePreviousStep?: () => void; + setCurrentSelectedProduct: Dispatch>; + setAddedUserList: Dispatch>>; + isAddInBulkEAFlow: boolean; + setIsAddInBulkEAFlow: Dispatch>; +}; + +export default function AddUserForm({ + party, + userId, + selectedProduct, + products, + productsRolesMap, + canEditRegistryData, + initialFormData, + goBack, + forwardNextStep, + setCurrentSelectedProduct, + setAddedUserList, + isAddInBulkEAFlow, + setIsAddInBulkEAFlow, +}: Readonly) { + const { t } = useTranslation(); + const setLoadingSaveUser = useLoading(LOADING_TASK_SAVE_PARTY_USER); + const setLoadingFetchTaxCode = useLoading(LOADING_TASK_FETCH_TAX_CODE); + + const addError = useErrorDispatcher(); + const addNotify = useUserNotify(); + const isMobile = useIsMobile('lg'); + + const history = useHistory(); + + // const [validTaxcode, setValidTaxcode] = useState(); + const [userProduct, setUserProduct] = useState(); + const [productRoles, setProductRoles] = useState(); + const [productInPage, setProductInPage] = useState(); + const [isAsyncFlow, setIsAsyncFlow] = useState(false); + const [dynamicDocLink, setDynamicDocLink] = useState(''); + + const { registerUnloadEvent, unregisterUnloadEvent } = useUnloadEventInterceptor(); + const { hasPermission } = usePermissions(); + const onExit = useUnloadEventOnExit(); + const { isPnpgTheOnlyProduct, pnpgProduct, activeOnboardings, isAdminEaOnProdIO } = + useAddUserFormComputedValues(party, products); + const { validate, validTaxcode, setValidTaxcode } = useFormValidation(t); + + useEffect(() => { + if (!initialFormData.taxCode) { + if (validTaxcode && validTaxcode !== initialFormData.taxCode) { + fetchTaxCode(validTaxcode, party.partyId); + } else if ( + !validTaxcode && + formik.values.certifiedName === true && + formik.values.certifiedSurname === true + ) { + void formik.setValues( + { + ...formik.values, + name: formik.initialValues.name, + surname: formik.initialValues.surname, + email: formik.initialValues.email, + confirmEmail: '', + certifiedName: formik.initialValues.certifiedName, + certifiedSurname: formik.initialValues.certifiedSurname, + }, + true + ); + } + } + }, [validTaxcode]); + + useEffect(() => { + if (initialFormData.taxCode) { + setValidTaxcode(initialFormData.taxCode); + } + }, [initialFormData]); + + useEffect(() => { + setUserProduct(selectedProduct); + }, [selectedProduct]); + + useEffect(() => { + if (isPnpgTheOnlyProduct && initialFormData.taxCode === '') { + setUserProduct(pnpgProduct); + } + }, []); + + useEffect(() => { + if (userProduct) { + setCurrentSelectedProduct(userProduct); + + const matchingProduct = activeOnboardings.find((p) => p.productId === userProduct.id); + + const institutionType = matchingProduct?.institutionType ?? party.institutionType; + + setDynamicDocLink(getProductLink(userProduct.id, institutionType)); + } + }, [userProduct, activeOnboardings, party.institutionType]); + + const goBackInner = + goBack ?? + (() => + history.push( + resolvePathVariables( + selectedProduct + ? DASHBOARD_USERS_ROUTES.PARTY_PRODUCT_USERS.path + : DASHBOARD_USERS_ROUTES.PARTY_USERS.path, + { + partyId: party.partyId, + productId: userProduct?.id ?? '', + } + ) + )); + + useEffect(() => { + const isEnabled = products.filter((p) => + party.products.some( + (pp) => + p.id === pp.productId && + hasPermission(pp.productId || '', Actions.CreateProductUsers) && + pp.productOnBoardingStatus === 'ACTIVE' + ) + ); + + setProductInPage(Object.keys(isEnabled).length === 1); + if (productInPage) { + setUserProduct(isEnabled[0]); + } + }, [productInPage]); + + const saveUser = useSaveUser({ + party, + userProduct, + productRoles, + t, + addNotify, + addError, + history, + setLoadingSaveUser, + unregisterUnloadEvent, + initialFormData, + selectedProduct, + isPnpgTheOnlyProduct, + }); + + const checkOnboardedUser = useCheckOnboardedUser({ + partyId: party.partyId, + userProductId: userProduct?.id, + t, + addError, + addNotify, + forwardNextStep, + setAddedUserList, + isAddInBulkEAFlow, + isAsyncFlow, + productRoles, + }); + + const errorNotify = (errors: any, taxCode: string) => + addError({ + id: 'FETCH_TAX_CODE', + blocking: false, + error: errors, + techDescription: `An error occurred while fetching Fiscal Code of Product ${taxCode}`, + toNotify: true, + }); + + const fetchTaxCode = (taxCode: string, partyId: string) => { + setLoadingFetchTaxCode(true); + fetchUserRegistryByFiscalCode(taxCode.toUpperCase(), partyId) + .then((userRegistry) => { + buildFormValues(userRegistry, formik.values, initialFormData); + }) + .catch((errors) => errorNotify(errors, taxCode)) + .finally(() => setLoadingFetchTaxCode(false)); + }; + + const addOneRoleModal = (values: PartyUserOnCreation) => { + addNotify({ + component: 'SessionModal', + id: 'ONE_ROLE_USER', + title: t('userEdit.addForm.addOneRoleModal.title'), + message: ( + productRoles?.groupByProductRole[r].title)}`, + productTitle: `${userProduct?.title}`, + }} + components={{ + 1: , + 3: , + 5: , + }} + > + {`Vuoi assegnare a <1>{{user}} il ruolo di <3>{{role}} per <5>{{productTitle}}?<7><8><9>`} + + ), + onConfirm: () => + saveUser( + { + ...values, + taxCode: values.taxCode.toUpperCase(), + email: values.email.toLowerCase(), + }, + userId + ), + confirmLabel: t('userEdit.addForm.addOneRoleModal.confirmButton'), + closeLabel: t('userEdit.addForm.addOneRoleModal.closeButton'), + }); + }; + + const formik = useFormik({ + initialValues: initialFormData, + validate, + onSubmit: (values: PartyUserOnCreation) => { + if (isAsyncFlow || isAddInBulkEAFlow) { + checkOnboardedUser(values); + return; + } + + if (values.productRoles.length >= 2) { + addMultiRoleModal(values); + } else { + addOneRoleModal(values); + } + }, + }); + + useEffect(() => { + if (formik.dirty || userProduct) { + registerUnloadEvent(); + } else { + unregisterUnloadEvent(); + } + }, [formik.dirty, userProduct]); + + useEffect(() => { + if (userProduct) { + setProductRoles(productsRolesMap[userProduct.id]); + void formik.setFieldValue('productRoles', [], true); + } + }, [userProduct]); + + const addMultiRoleModal = (values: PartyUserOnCreation) => { + addNotify({ + component: 'SessionModal', + id: 'MULTI_ROLE_USER', + title: t('userEdit.addForm.addMultiRoleModal.title'), + message: ( + productRoles?.groupByProductRole[r].title) + .join(', ')}`, + productTitle: `${userProduct?.title}.`, + }} + components={{ 1: , 3: , 5: }} + > + {`Stai per assegnare a <1>{{user}} i ruoli <3>{{roles}} sul prodotto <5>{{productTitle}}<6><7><8>Confermi di voler continuare?<9>`} + + ), + // eslint-disable-next-line sonarjs/no-identical-functions + onConfirm: () => + saveUser( + { + ...values, + taxCode: values.taxCode.toUpperCase(), + email: values.email.toLowerCase(), + }, + userId + ), + confirmLabel: t('userEdit.addForm.addMultiRoleModal.confirmButton'), + closeLabel: t('userEdit.addForm.addMultiRoleModal.closeButton'), + }); + }; + + const addRole = async (r: ProductRole) => { + // eslint-disable-next-line functional/no-let + let nextProductRoles; + if (r.multiroleAllowed && formik.values.productRoles.length > 0) { + if (productRoles?.groupByProductRole[formik.values.productRoles[0]].selcRole !== r.selcRole) { + nextProductRoles = [r.productRole]; + } else { + const productRoleIndex = formik.values.productRoles.findIndex((p) => p === r.productRole); + if (productRoleIndex === -1) { + nextProductRoles = formik.values.productRoles.concat([r.productRole]); + } else { + nextProductRoles = formik.values.productRoles.filter((_p, i) => i !== productRoleIndex); + } + } + } else { + nextProductRoles = [r.productRole]; + } + await formik.setFieldValue('productRoles', nextProductRoles, true); + }; + + const baseTextFieldProps = ( + field: keyof PartyUserOnCreation, + label: string, + placeholder: string, + textTransform?: TextTransform + ) => { + const isError = !!formik.errors[field] && formik.errors[field] !== requiredError; + + return { + id: field, + type: 'text', + value: formik.values[field], + label, + placeholder, + error: isError, + helperText: isError ? formik.errors[field] : undefined, + required: true, + variant: 'outlined' as const, + onChange: formik.handleChange, + sx: { width: '100%' }, + InputProps: { + style: { + fontSize: 'fontSize', + fontWeight: 'fontWeightMedium', + lineHeight: '24px', + color: 'text.primary', + textAlign: 'start' as const, + }, + }, + inputProps: { + style: { + textTransform, + }, + }, + }; + }; + + const selectLabel = t('userEdit.addForm.product.selectLabel'); + + return ( +
+ {canEditRegistryData && ( + + )} + + {!selectedProduct && !isPnpgTheOnlyProduct && ( + + )} + + {productRoles && ( + + )} + {isAdminEaOnProdIO && + userProduct?.id === PRODUCT_IDS.IO && + formik.values.productRoles.length > 0 && ( + + + + + + + {EA_RADIO_OPTIONS.map(({ value, titleKey, descriptionKey }) => ( + } + label={ + + } + aria-label={t(titleKey)} + /> + ))} + + + + )} + + + + + + + ); +} diff --git a/src/pages/addUserFlow/addUser/components/AddUserForm/components/ProductRolesSection.tsx b/src/pages/addUserFlow/addUser/components/AddUserForm/components/ProductRolesSection.tsx new file mode 100644 index 00000000..9e4d8488 --- /dev/null +++ b/src/pages/addUserFlow/addUser/components/AddUserForm/components/ProductRolesSection.tsx @@ -0,0 +1,121 @@ +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import { Box, Checkbox, Divider, Grid, Radio, Tooltip } from '@mui/material'; +import { ButtonNaked } from '@pagopa/mui-italia'; +import { TitleBox } from '@pagopa/selfcare-common-frontend/lib'; +import React, { SetStateAction } from 'react'; +import { Party } from '../../../../../../model/Party'; +import { Product } from '../../../../../../model/Product'; +import { ProductRole, ProductRolesLists } from '../../../../../../model/ProductRole'; +import { commonStyles, CustomFormControlLabel } from '../../../../utils/helpers'; +import { isAddRoleFromDashboard, isAddRoleFromDashboardAsync } from '../utils/addUserFormUtils'; + +// 3. ProductRolesSection Component +interface ProductRolesSectionProps { + party: Party; + productRoles: ProductRolesLists; + dynamicDocLink: string; + formik: any; + validTaxcode: string | undefined; + addRole: (role: ProductRole) => void; + setIsAddInBulkEAFlow: (value: SetStateAction) => void; + setIsAsyncFlow: (value: SetStateAction) => void; + userProduct: Product | undefined; + renderLabel: (role: ProductRole, enabled: boolean) => any; + t: (key: string) => string; +} + +export const ProductRolesSection = ({ + productRoles, + dynamicDocLink, + formik, + validTaxcode, + addRole, + setIsAddInBulkEAFlow, + setIsAsyncFlow, + userProduct, + party, + renderLabel, + + t, +}: ProductRolesSectionProps) => ( + + + {dynamicDocLink.length > 0 && ( + + { + window.open(dynamicDocLink); + }} + > + {t('userEdit.addForm.role.documentationLink')} + + + )} + {Object.values(productRoles.groupBySelcRole).map((roles) => + roles + .filter((r) => isAddRoleFromDashboard(r.phasesAdditionAllowed)) + .map((p, index: number, filteredRoles) => ( + + + -1} + disabled={!validTaxcode} + value={p.productRole} + control={roles.length > 1 && p.multiroleAllowed ? : } + label={renderLabel(p, !!validTaxcode)} + aria-label={`${p.title}`} + onClick={ + validTaxcode + ? () => { + addRole(p); + setIsAddInBulkEAFlow( + p?.phasesAdditionAllowed.includes('dashboard-aggregator') && + party.products.some( + (p) => p.productId === userProduct?.id && p.isAggregator + ) + ); + setIsAsyncFlow(p?.phasesAdditionAllowed.includes('dashboard-async')); + } + : undefined + } + /> + {isAddRoleFromDashboardAsync(p?.phasesAdditionAllowed) && ( + + + + )} + + {filteredRoles.length !== index + 1 && ( + + + + )} + + )) + )} + +); diff --git a/src/pages/addUserFlow/addUser/components/AddUserForm/components/ProductSelectionSection.tsx b/src/pages/addUserFlow/addUser/components/AddUserForm/components/ProductSelectionSection.tsx new file mode 100644 index 00000000..0c290a23 --- /dev/null +++ b/src/pages/addUserFlow/addUser/components/AddUserForm/components/ProductSelectionSection.tsx @@ -0,0 +1,108 @@ +import { + FormControl, + Grid, + InputLabel, + MenuItem, + OutlinedInput, + Select, + Typography, +} from '@mui/material'; +import { TitleBox } from '@pagopa/selfcare-common-frontend/lib'; +import { Actions } from '@pagopa/selfcare-common-frontend/lib/utils/constants'; +import { Party } from '../../../../../../model/Party'; +import { Product } from '../../../../../../model/Product'; +import { commonStyles } from '../../../../utils/helpers'; + +interface ProductSelectionSectionProps { + products: Array; + party: Party; + hasPermission: (productId: string, action: string) => boolean; + userProduct: Product | undefined; + setUserProduct: (product: Product | undefined) => void; + selectLabel: string; + validTaxcode: string | undefined; + productInPage: boolean | undefined; + t: (key: string) => string; +} + +export const ProductSelectionSection = ({ + products, + party, + hasPermission, + userProduct, + setUserProduct, + selectLabel, + validTaxcode, + productInPage, + t, +}: ProductSelectionSectionProps) => ( + + + + + + + + {selectLabel} + + + + + +); diff --git a/src/pages/addUserFlow/addUser/components/AddUserForm/components/UserDataSection.tsx b/src/pages/addUserFlow/addUser/components/AddUserForm/components/UserDataSection.tsx new file mode 100644 index 00000000..bbf9f7de --- /dev/null +++ b/src/pages/addUserFlow/addUser/components/AddUserForm/components/UserDataSection.tsx @@ -0,0 +1,95 @@ +import { Grid } from '@mui/material'; +import { theme } from '@pagopa/mui-italia'; +import { TitleBox } from '@pagopa/selfcare-common-frontend/lib'; +import { PartyUserOnCreation, TextTransform } from '../../../../../../model/PartyUser'; +import { commonStyles, CustomTextField } from '../../../../utils/helpers'; + +type UserDataSectionProps = { + formik: any; + baseTextFieldProps: ( + field: keyof PartyUserOnCreation, + label: string, + placeholder: string, + textTransform?: TextTransform + ) => any; + validTaxcode: string | undefined; + isMobile: boolean; + t: (key: string) => string; +}; + +export const UserDataSection = ({ + formik, + baseTextFieldProps, + validTaxcode, + isMobile, + t, +}: UserDataSectionProps) => ( + + + + + + + + + + + + + + + + + + +); diff --git a/src/pages/addUserFlow/addUser/components/AddUserForm/hooks/useAddUserFormLogic.tsx b/src/pages/addUserFlow/addUser/components/AddUserForm/hooks/useAddUserFormLogic.tsx new file mode 100644 index 00000000..ac9a0479 --- /dev/null +++ b/src/pages/addUserFlow/addUser/components/AddUserForm/hooks/useAddUserFormLogic.tsx @@ -0,0 +1,43 @@ +import { useState } from 'react'; +import { Party } from '../../../../../../model/Party'; +import { PartyUserOnCreation } from '../../../../../../model/PartyUser'; +import { Product } from '../../../../../../model/Product'; +import { validateUserForm } from '../utils/addUserFormUtils'; + +export const useAddUserFormComputedValues = (party: Party, products: Array) => { + const isPnpgTheOnlyProduct = + !!products.find((p) => p.id === 'prod-pn-pg') && products.length === 1; + const pnpgProduct = products.find((p) => p.id === 'prod-pn-pg'); + + const activeOnboardings = party.products.filter((p) => p.productOnBoardingStatus === 'ACTIVE'); + + const isAdminEaOnProdIO = party.products.some( + (p) => p.productOnBoardingStatus === 'ACTIVE' && p.productId === 'prod-io' && p.isAggregator + ); + + return { + isPnpgTheOnlyProduct, + pnpgProduct, + activeOnboardings, + isAdminEaOnProdIO, + }; +}; + +// Custom hook for form validation +export const useFormValidation = (t: any) => { + const [validTaxcode, setValidTaxcode] = useState(); + + const validate = (values: Partial) => { + const errors = validateUserForm(values, t); + + if (!errors.taxCode) { + setValidTaxcode(values.taxCode); + } else { + setValidTaxcode(undefined); + } + + return errors; + }; + + return { validate, validTaxcode, setValidTaxcode }; +}; diff --git a/src/pages/addUserFlow/addUser/components/AddUserForm/utils/addUserFormUtils.ts b/src/pages/addUserFlow/addUser/components/AddUserForm/utils/addUserFormUtils.ts new file mode 100644 index 00000000..ce9b0870 --- /dev/null +++ b/src/pages/addUserFlow/addUser/components/AddUserForm/utils/addUserFormUtils.ts @@ -0,0 +1,116 @@ +import { emailRegexp } from '@pagopa/selfcare-common-frontend/lib/utils/constants'; +import { verifyChecksumMatchWithTaxCode } from '@pagopa/selfcare-common-frontend/lib/utils/verifyChecksumMatchWithTaxCode'; +import { verifyNameMatchWithTaxCode } from '@pagopa/selfcare-common-frontend/lib/utils/verifyNameMatchWithTaxCode'; +import { verifySurnameMatchWithTaxCode } from '@pagopa/selfcare-common-frontend/lib/utils/verifySurnameMatchWithTaxCode'; +import { PartyUserOnCreation } from '../../../../../../model/PartyUser'; +import { UserRegistry } from '../../../../../../model/UserRegistry'; +import { requiredError, taxCodeRegexp } from '../../../../utils/validation'; + +// Pure validation utilities +export const validateUserForm = ( + values: Partial, + t: (key: string) => string + // eslint-disable-next-line sonarjs/cognitive-complexity +) => + Object.fromEntries( + Object.entries({ + name: !values.name + ? requiredError + : verifyNameMatchWithTaxCode(values.name, values.taxCode) + ? t('userEdit.mismatchWithTaxCode.name') + : undefined, + surname: !values.surname + ? requiredError + : verifySurnameMatchWithTaxCode(values.surname, values.taxCode) + ? t('userEdit.mismatchWithTaxCode.surname') + : undefined, + taxCode: !values.taxCode + ? requiredError + : !taxCodeRegexp.test(values.taxCode) || verifyChecksumMatchWithTaxCode(values.taxCode) + ? t('userEdit.addForm.errors.invalidFiscalCode') + : undefined, + email: !values.email + ? requiredError + : !emailRegexp.test(values.email) + ? t('userEdit.addForm.errors.invalidEmail') + : undefined, + confirmEmail: !values.confirmEmail + ? requiredError + : values.email && + values.confirmEmail.toLocaleLowerCase() !== values.email.toLocaleLowerCase() + ? t('userEdit.addForm.errors.mismatchEmail') + : undefined, + productRoles: values.productRoles?.length === 0 ? requiredError : undefined, + }).filter(([_key, value]) => value) + ); + +// Pure utility functions +export const isAddRoleFromDashboard = (phasesAdditionAllowed?: Array) => + !!phasesAdditionAllowed && phasesAdditionAllowed[0].startsWith('dashboard'); + +export const isAddRoleFromDashboardAsync = (phasesAdditionAllowed?: Array) => + !!phasesAdditionAllowed && phasesAdditionAllowed[0] === 'dashboard-async'; + +export const EA_RADIO_OPTIONS = [ + { + value: true, + titleKey: 'userEdit.addForm.addOnAggregatedEntities.radioTitle1', + descriptionKey: 'userEdit.addForm.addOnAggregatedEntities.radioDescription1', + }, + { + value: false, + titleKey: 'userEdit.addForm.addOnAggregatedEntities.radioTitle2', + descriptionKey: 'userEdit.addForm.addOnAggregatedEntities.radioDescription2', + }, +]; + +export const buildFormValues = ( + userRegistry: UserRegistry | null, + currentValues: any, + initialFormData: any +) => ({ + ...currentValues, + name: + userRegistry?.name ?? (currentValues.certifiedName ? initialFormData.name : currentValues.name), + surname: + userRegistry?.surname ?? + (currentValues.certifiedSurname ? initialFormData.surname : currentValues.surname), + email: + userRegistry?.email ?? + (currentValues.certifiedName || currentValues.certifiedSurname + ? initialFormData.email + : currentValues.email), + confirmEmail: '', + certifiedName: + userRegistry?.certifiedName ?? + (currentValues.certifiedName ? initialFormData.certifiedName : currentValues.certifiedName), + certifiedSurname: + userRegistry?.certifiedSurname ?? + (currentValues.certifiedSurname + ? initialFormData.certifiedSurname + : currentValues.certifiedSurname), +}); + +/* +// Role management utilities +export const calculateNextProductRoles = ( + role: ProductRole, + currentRoles: Array, + productRoles: ProductRolesLists | undefined +) => { + if (role.multiroleAllowed && currentRoles.length > 0) { + if (productRoles?.groupByProductRole[currentRoles[0]].selcRole !== role.selcRole) { + return [role.productRole]; + } else { + const productRoleIndex = currentRoles.findIndex((p) => p === role.productRole); + if (productRoleIndex === -1) { + return currentRoles.concat([role.productRole]); + } else { + return currentRoles.filter((_p, i) => i !== productRoleIndex); + } + } + } else { + return [role.productRole]; + } +}; +*/ diff --git a/src/pages/addUserFlow/utils/helpers.tsx b/src/pages/addUserFlow/utils/helpers.tsx index 851aeda4..f8bfc4a4 100644 --- a/src/pages/addUserFlow/utils/helpers.tsx +++ b/src/pages/addUserFlow/utils/helpers.tsx @@ -1,4 +1,4 @@ -import { styled, TextField, Typography } from '@mui/material'; +import { FormControlLabel, styled, TextField, Typography } from '@mui/material'; import { IllusCompleted, IllusError } from '@pagopa/mui-italia'; import { EndingPage } from '@pagopa/selfcare-common-frontend/lib'; import { Trans } from 'react-i18next'; @@ -176,3 +176,42 @@ export const renderLabel = (p: ProductRole, validTaxcode: boolean) => ( ); +export const CustomFormControlLabel = styled(FormControlLabel)({ + disabled: false, + '.MuiRadio-root': { + color: '#0073E6', + }, +}); + +type RadioOptionLabelProps = { + titleKey: string; + descriptionKey: string; + disabled: boolean; + t: (t: string) => string; +}; + +export const RadioOptionLabel = ({ titleKey, descriptionKey, disabled, t }: RadioOptionLabelProps) => ( + <> + + {t(titleKey)} + + + {t(descriptionKey)} + + +); \ No newline at end of file