diff --git a/__mocks__/data/purpose.mocks.ts b/__mocks__/data/purpose.mocks.ts index 55336dbb7..8630e173a 100644 --- a/__mocks__/data/purpose.mocks.ts +++ b/__mocks__/data/purpose.mocks.ts @@ -198,6 +198,65 @@ const createMockPurposeUsesPersonalDataAnswerYes = createMockFactory({ rulesetExpiration: '2030-01-01T00:00:00Z', }) +const createMockPurposeCallsExceed = createMockFactory({ + id: 'purpose-id', + title: 'Test Purpose', + consumer: { id: 'consumer-id', name: 'Consumer Name' }, + eservice: { + id: 'eservice-id', + name: 'Test Eservice', + mode: 'DELIVER', + producer: { id: 'producer-id', name: 'Producer Name' }, + personalData: false, + descriptor: { + id: 'descriptor-id', + state: 'PUBLISHED', + version: '1', + audience: ['test'], + }, + }, + agreement: { id: 'agreement-id', state: 'ACTIVE', canBeUpgraded: false }, + riskAnalysisForm: { + answers: { usesPersonalData: ['YES'] }, + version: '3.1', + riskAnalysisId: 'risk-analysis-id', + }, + versions: [ + { + createdAt: '2023-02-03T07:59:52.458Z', + dailyCalls: 1, + firstActivationAt: '2023-02-03T08:26:43.139Z', + id: '3a5c9422-876c-4de8-828a-66586fd68b55', + riskAnalysisDocument: { + contentType: 'application/pdf', + createdAt: '2023-02-03T08:26:43.049Z', + id: '3562b028-0193-45fa-acf9-4bbe1ced352a', + }, + state: 'ACTIVE', + }, + ], + clients: [], + description: '', + isFreeOfCharge: false, + dailyCallsPerConsumer: 0, + currentVersion: { + createdAt: '2023-02-03T07:59:52.458Z', + dailyCalls: 1, + firstActivationAt: '2023-02-03T08:26:43.139Z', + id: '3a5c9422-876c-4de8-828a-66586fd68b55', + riskAnalysisDocument: { + contentType: 'application/pdf', + createdAt: '2023-02-03T08:26:43.049Z', + id: '3562b028-0193-45fa-acf9-4bbe1ced352a', + }, + state: 'ACTIVE', + }, + dailyCallsTotal: 0, + hasUnreadNotifications: false, + isDocumentReady: false, + rulesetExpiration: undefined, +}) + const createMockPurposeCompatiblePersonalDataYes = createMockFactory({ ...createMockPurposeUsesPersonalDataAnswerNo(), riskAnalysisForm: { @@ -214,6 +273,42 @@ const createMockPurposeCompatiblePersonalDataNo = createMockFactory({ }, }) +const createMockPurposeCallsPerConsumerExceed = createMockFactory({ + ...createMockPurposeCallsExceed(), + currentVersion: { + id: '1', + state: 'ACTIVE', + createdAt: '2023-02-03T07:59:52.458Z', + dailyCalls: 2, + }, + dailyCallsPerConsumer: 1, + dailyCallsTotal: 10, +}) + +const createMockPurposeCallsTotalExceed = createMockFactory({ + ...createMockPurposeCallsExceed(), + currentVersion: { + id: '1', + state: 'ACTIVE', + createdAt: '2023-02-03T07:59:52.458Z', + dailyCalls: 2, + }, + dailyCallsPerConsumer: 10, + dailyCallsTotal: 1, +}) + +const createMockPurposeCallsWithoutExceed = createMockFactory({ + ...createMockPurposeCallsExceed(), + currentVersion: { + id: '1', + state: 'ACTIVE', + createdAt: '2023-02-03T07:59:52.458Z', + dailyCalls: 1, + }, + dailyCallsPerConsumer: 10, + dailyCallsTotal: 100, +}) + export { createMockPurpose, createMockRiskAnalysisFormConfig, @@ -221,4 +316,7 @@ export { createMockPurposeUsesPersonalDataAnswerYes, createMockPurposeCompatiblePersonalDataYes, createMockPurposeCompatiblePersonalDataNo, + createMockPurposeCallsPerConsumerExceed, + createMockPurposeCallsTotalExceed, + createMockPurposeCallsWithoutExceed, } diff --git a/src/components/shared/DelegationTooltip.tsx b/src/components/shared/DelegationTooltip.tsx new file mode 100644 index 000000000..9a55e44f1 --- /dev/null +++ b/src/components/shared/DelegationTooltip.tsx @@ -0,0 +1,36 @@ +import { type DelegationWithCompactTenants } from '@/api/api.generatedTypes' +import { AuthHooks } from '@/api/auth' +import { AltRoute } from '@mui/icons-material' +import { Tooltip } from '@mui/material' +import React from 'react' +import { useTranslation } from 'react-i18next' + +type DelegationTooltipProps = { + delegation: DelegationWithCompactTenants +} + +export const DelegationTooltip: React.FC = ({ delegation }) => { + const { t } = useTranslation('shared-components', { keyPrefix: 'delegationTooltip' }) + const { jwt } = AuthHooks.useJwt() + + const isDelegator = Boolean(delegation.delegator.id === jwt?.organizationId) + const delegator = delegation.delegator.name + const delegate = delegation.delegate.name + + const label = isDelegator + ? t('label.delegator', { delegate }) + : t('label.delegate', { delegator }) + + return ( + + + + ) +} diff --git a/src/components/shared/GreyAlert.tsx b/src/components/shared/GreyAlert.tsx new file mode 100644 index 000000000..00abe7345 --- /dev/null +++ b/src/components/shared/GreyAlert.tsx @@ -0,0 +1,14 @@ +import { Alert } from '@mui/material' +import React from 'react' + +interface GreyAlertProps { + children: React.ReactNode +} + +export const GreyAlert: React.FC = ({ children }) => { + return ( + + {children} + + ) +} diff --git a/src/components/shared/SummaryAccordion.tsx b/src/components/shared/SummaryAccordion.tsx index 1edfeef50..5039d10a6 100644 --- a/src/components/shared/SummaryAccordion.tsx +++ b/src/components/shared/SummaryAccordion.tsx @@ -41,9 +41,9 @@ export const SummaryAccordion: React.FC = ({ }, }} > - + {headline} - + {title} diff --git a/src/components/shared/react-hook-form-inputs/RHFAutocomplete/_RHFAutocompleteBase.tsx b/src/components/shared/react-hook-form-inputs/RHFAutocomplete/_RHFAutocompleteBase.tsx index fccf0c197..efeb5cd96 100644 --- a/src/components/shared/react-hook-form-inputs/RHFAutocomplete/_RHFAutocompleteBase.tsx +++ b/src/components/shared/react-hook-form-inputs/RHFAutocomplete/_RHFAutocompleteBase.tsx @@ -118,6 +118,7 @@ export function _RHFAutocompleteBase< { const { t } = useTranslation('purpose') @@ -16,6 +17,15 @@ const ConsumerPurposeCreatePage: React.FC = () => { to: 'SUBSCRIBE_PURPOSE_LIST', }} > + + {t('create.requiredLabel')} + ) diff --git a/src/pages/ConsumerPurposeCreatePage/__tests__/ConsumerPurposeCreate.page.test.tsx b/src/pages/ConsumerPurposeCreatePage/__tests__/ConsumerPurposeCreate.page.test.tsx new file mode 100644 index 000000000..74c50f544 --- /dev/null +++ b/src/pages/ConsumerPurposeCreatePage/__tests__/ConsumerPurposeCreate.page.test.tsx @@ -0,0 +1,34 @@ +import { describe, it, expect, vi } from 'vitest' +import { screen } from '@testing-library/react' +import ConsumerPurposeCreatePage from '../ConsumerPurposeCreate.page' +import { mockUseJwt, renderWithApplicationContext } from '@/utils/testing.utils' + +mockUseJwt() +const useQueryMock = vi.fn() + +describe('ConsumerPurposeCreatePage', () => { + it('renders page title', () => { + useQueryMock.mockReturnValue({ + isLoading: false, + }) + + renderWithApplicationContext(, { + withReactQueryContext: true, + withRouterContext: true, + }) + + expect(screen.getByText('create.emptyTitle')).toBeInTheDocument() + }) + it('renders required label', () => { + useQueryMock.mockReturnValue({ + isLoading: false, + }) + + renderWithApplicationContext(, { + withReactQueryContext: true, + withRouterContext: true, + }) + + expect(screen.getByText('create.requiredLabel')).toBeInTheDocument() + }) +}) diff --git a/src/pages/ConsumerPurposeEditPage/ConsumerPurposeEdit.page.tsx b/src/pages/ConsumerPurposeEditPage/ConsumerPurposeEdit.page.tsx index 429202934..e271a4f7f 100644 --- a/src/pages/ConsumerPurposeEditPage/ConsumerPurposeEdit.page.tsx +++ b/src/pages/ConsumerPurposeEditPage/ConsumerPurposeEdit.page.tsx @@ -9,6 +9,7 @@ import { PurposeEditStepRiskAnalysis } from './components/PurposeEditStepRiskAna import { useParams, useNavigate } from '@/router' import { PurposeQueries } from '@/api/purpose' import { useQuery } from '@tanstack/react-query' +import { Typography } from '@mui/material' const ConsumerPurposeEditPage: React.FC = () => { const { t } = useTranslation('purpose') @@ -60,6 +61,15 @@ const ConsumerPurposeEditPage: React.FC = () => { to: 'SUBSCRIBE_PURPOSE_LIST', }} > + + {t('create.requiredLabel')} + {!isReceive && } diff --git a/src/pages/ConsumerPurposeEditPage/components/PurposeEditStepGeneral/PurposeEditStepGeneralForm.tsx b/src/pages/ConsumerPurposeEditPage/components/PurposeEditStepGeneral/PurposeEditStepGeneralForm.tsx index f8f61affb..c32ddc23c 100644 --- a/src/pages/ConsumerPurposeEditPage/components/PurposeEditStepGeneral/PurposeEditStepGeneralForm.tsx +++ b/src/pages/ConsumerPurposeEditPage/components/PurposeEditStepGeneral/PurposeEditStepGeneralForm.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Box } from '@mui/material' +import { Alert, AlertTitle, Box, Stack, Typography } from '@mui/material' import { FormProvider, useForm } from 'react-hook-form' import { RHFRadioGroup, RHFTextField } from '@/components/shared/react-hook-form-inputs' import { useTranslation } from 'react-i18next' @@ -10,6 +10,8 @@ import type { ActiveStepProps } from '@/hooks/useActiveStep' import type { Purpose, PurposeUpdateContent } from '@/api/api.generatedTypes' import SaveIcon from '@mui/icons-material/Save' import { useNavigate } from '@/router' +import { useGetConsumerPurposeEditPageInfoAlertProps } from '../../hooks/useGetConsumerPurposeEditPageInfoAlertProps' +import { GreyAlert } from '@/components/shared/GreyAlert' export type PurposeEditStepGeneralFormValues = Omit< PurposeUpdateContent, @@ -75,6 +77,18 @@ const PurposeEditStepGeneralForm: React.FC = ({ const isFreeOfCharge = formMethods.watch('isFreeOfCharge') + const dailyCallsFormValue = formMethods.watch('dailyCalls') + + const dailyCallsPerConsumer = purpose.dailyCallsPerConsumer + + const dailyCallsTotal = purpose.dailyCallsTotal + + const alertProps = useGetConsumerPurposeEditPageInfoAlertProps( + dailyCallsFormValue, + dailyCallsPerConsumer, + dailyCallsTotal + ) + return ( @@ -86,6 +100,7 @@ const PurposeEditStepGeneralForm: React.FC = ({ focusOnMount inputProps={{ maxLength: 60 }} rules={{ required: true, minLength: 5 }} + required /> = ({ multiline inputProps={{ maxLength: 250 }} rules={{ required: true, minLength: 10 }} + required /> = ({ rules={{ required: true, minLength: 10 }} /> )} - + + + {alertProps && } + + + {t('edit.loadEstimationSection.providerThresholdsInfo.label')} + + + + + {t( + 'edit.loadEstimationSection.providerThresholdsInfo.dailyCallsPerConsumer.label' + )} + + + {t( + 'edit.loadEstimationSection.providerThresholdsInfo.dailyCallsPerConsumer.value', + { + min: '#' /* @TODO - add residual threshold */, + max: dailyCallsPerConsumer, + } + )} + + + + + {t('edit.loadEstimationSection.providerThresholdsInfo.dailyCallsTotal.label')} + + + {t('edit.loadEstimationSection.providerThresholdsInfo.dailyCallsTotal.value', { + min: '#' /* @TODO - add residual threshold */, + max: dailyCallsTotal, + })} + + + + + {t('edit.loadEstimationSection.providerThresholdsInfo.description')} + + { + it('should return undefined if there is no exceed', () => { + const { result } = renderHook(() => useGetConsumerPurposeEditPageInfoAlertProps(1, 10, 100)) + expect(result.current).toStrictEqual(undefined) + }) + it('should return infoDailyCallsPerConsumerExceed if dailyCalls > dailyCallsPerConsumer', () => { + const { result } = renderHook(() => useGetConsumerPurposeEditPageInfoAlertProps(11, 10, 100)) + expect(result.current).toStrictEqual({ + children: 'infoDailyCallsPerConsumerExceed', + severity: 'info', + }) + }) + it('should return infoDailyCallsTotalExceed if dailyCalls > dailyCallsTotal', () => { + const { result } = renderHook(() => useGetConsumerPurposeEditPageInfoAlertProps(111, 10, 100)) + expect(result.current).toStrictEqual({ + children: 'infoDailyCallsTotalExceed', + severity: 'info', + }) + }) +}) diff --git a/src/pages/ConsumerPurposeEditPage/hooks/useGetConsumerPurposeEditPageInfoAlertProps.tsx b/src/pages/ConsumerPurposeEditPage/hooks/useGetConsumerPurposeEditPageInfoAlertProps.tsx new file mode 100644 index 000000000..97a4fbc6f --- /dev/null +++ b/src/pages/ConsumerPurposeEditPage/hooks/useGetConsumerPurposeEditPageInfoAlertProps.tsx @@ -0,0 +1,27 @@ +import type { AlertProps } from '@mui/material' +import { useTranslation } from 'react-i18next' +import { match } from 'ts-pattern' + +export function useGetConsumerPurposeEditPageInfoAlertProps( + dailyCalls: number, + dailyCallsPerConsumer: number, + dailyCallsTotal: number +): AlertProps | undefined { + const { t } = useTranslation('purpose', { keyPrefix: 'edit.loadEstimationSection.alerts' }) + + return match({ + isDailyCallsPerConsumerExceed: dailyCalls > dailyCallsPerConsumer, + isDailyCallsTotalExceed: dailyCalls > dailyCallsTotal, + }) + .returnType() + .with({ isDailyCallsTotalExceed: true }, () => ({ + severity: 'info', + children: t('infoDailyCallsTotalExceed'), + })) + .with({ isDailyCallsPerConsumerExceed: true }, () => ({ + severity: 'info', + children: t('infoDailyCallsPerConsumerExceed'), + })) + .otherwise(() => undefined) + /* @TODO - Add residual threshold cases */ +} diff --git a/src/pages/ConsumerPurposeSummaryPage/ConsumerPurposeSummary.page.tsx b/src/pages/ConsumerPurposeSummaryPage/ConsumerPurposeSummary.page.tsx index f09a9e31e..3a4ac93f3 100644 --- a/src/pages/ConsumerPurposeSummaryPage/ConsumerPurposeSummary.page.tsx +++ b/src/pages/ConsumerPurposeSummaryPage/ConsumerPurposeSummary.page.tsx @@ -1,7 +1,7 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { useNavigate, useParams } from '@/router' -import { Alert, Button, Stack, Tooltip, Typography } from '@mui/material' +import { Alert, Button, Stack, Tooltip } from '@mui/material' import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline' import CreateIcon from '@mui/icons-material/Create' import PublishIcon from '@mui/icons-material/Publish' @@ -18,11 +18,8 @@ import { import { useGetConsumerPurposeAlertProps } from './hooks/useGetConsumerPurposeAlertProps' import { useQuery } from '@tanstack/react-query' import { AuthHooks } from '@/api/auth' -import { - checkIsRulesetExpired, - getDaysToExpiration, - getFormattedExpirationDate, -} from '@/utils/purpose.utils' +import { checkIsRulesetExpired } from '@/utils/purpose.utils' +import { ConsumerPurposeSummaryRiskAnalysisAlertContainer } from './components/ConsumerPurposeSummaryRiskAnalysisAlertContainer' const ConsumerPurposeSummaryPage: React.FC = () => { const { t } = useTranslation('purpose') @@ -38,8 +35,6 @@ const ConsumerPurposeSummaryPage: React.FC = () => { const expirationDate = purpose?.rulesetExpiration - const daysToExpiration = getDaysToExpiration(expirationDate) - const isRulesetExpired = checkIsRulesetExpired(expirationDate) const alertProps = useGetConsumerPurposeAlertProps(purpose) @@ -123,7 +118,7 @@ const ConsumerPurposeSummaryPage: React.FC = () => { return ( { - {expirationDate && !isRulesetExpired && ( - - {t('summary.alerts.infoRulesetExpiration', { - days: daysToExpiration, - date: getFormattedExpirationDate(expirationDate), - })} - - )} - {isRulesetExpired && ( - - - {' '} - {/**TODO FIX SPACING */} - {t('summary.alerts.rulesetExpired.label')} - - - - )} + diff --git a/src/pages/ConsumerPurposeSummaryPage/__tests__/ConsumerPurposeSummary.page.test.tsx b/src/pages/ConsumerPurposeSummaryPage/__tests__/ConsumerPurposeSummary.page.test.tsx index 551ad1bdc..d6a1b4397 100644 --- a/src/pages/ConsumerPurposeSummaryPage/__tests__/ConsumerPurposeSummary.page.test.tsx +++ b/src/pages/ConsumerPurposeSummaryPage/__tests__/ConsumerPurposeSummary.page.test.tsx @@ -63,11 +63,15 @@ vi.mock('./hooks/useGetConsumerPurposeAlertProps', () => ({ useGetConsumerPurposeAlertProps: () => undefined, })) -vi.mock('@/utils/purpose.utils', () => ({ - checkIsRulesetExpired: vi.fn(), - getDaysToExpiration: vi.fn(), - getFormattedExpirationDate: vi.fn(), -})) +vi.mock('@/utils/purpose.utils', async () => { + const actual = await vi.importActual('@/utils/purpose.utils') + return { + ...(actual as Record), + checkIsRulesetExpired: vi.fn(), + getDaysToExpiration: vi.fn(), + getFormattedExpirationDate: vi.fn(), + } +}) describe('ConsumerPurposeSummaryPage', () => { beforeEach(() => { @@ -88,7 +92,7 @@ describe('ConsumerPurposeSummaryPage', () => { withRouterContext: true, }) - expect(screen.getByText('Test Purpose')).toBeInTheDocument() + expect(screen.getByText('summary.title')).toBeInTheDocument() }) it('disables publish button when personal data answer is incompatible (case: user answer NO, eservice personalData true)', () => { @@ -103,7 +107,7 @@ describe('ConsumerPurposeSummaryPage', () => { }) const publishButton = screen.getByRole('button', { - name: 'publishDraft', + name: 'publish', }) expect(publishButton).toBeDisabled() @@ -121,7 +125,7 @@ describe('ConsumerPurposeSummaryPage', () => { }) const publishButton = screen.getByRole('button', { - name: 'publishDraft', + name: 'publish', }) expect(publishButton).toBeDisabled() @@ -139,7 +143,7 @@ describe('ConsumerPurposeSummaryPage', () => { }) const publishButton = screen.getByRole('button', { - name: 'publishDraft', + name: 'publish', }) expect(publishButton).toBeEnabled() @@ -157,7 +161,7 @@ describe('ConsumerPurposeSummaryPage', () => { }) const publishButton = screen.getByRole('button', { - name: 'publishDraft', + name: 'publish', }) expect(publishButton).toBeEnabled() @@ -188,7 +192,7 @@ describe('ConsumerPurposeSummaryPage', () => { withRouterContext: true, }) - expect(screen.queryByRole('alert')).not.toBeInTheDocument() + expect(screen.queryByText('summary.alerts.infoRulesetExpiration')).not.toBeInTheDocument() }) it('shows alert if isRulesetExpired is true', () => { vi.mocked(checkIsRulesetExpired).mockReturnValue(true) diff --git a/src/pages/ConsumerPurposeSummaryPage/components/ConsumerPurposeSummaryGeneralInformationAccordion.tsx b/src/pages/ConsumerPurposeSummaryPage/components/ConsumerPurposeSummaryGeneralInformationAccordion.tsx index 9c0c60502..6b7e1976c 100644 --- a/src/pages/ConsumerPurposeSummaryPage/components/ConsumerPurposeSummaryGeneralInformationAccordion.tsx +++ b/src/pages/ConsumerPurposeSummaryPage/components/ConsumerPurposeSummaryGeneralInformationAccordion.tsx @@ -1,4 +1,4 @@ -import { Stack } from '@mui/material' +import { Alert, Stack } from '@mui/material' import { InformationContainer } from '@pagopa/interop-fe-commons' import React from 'react' import { Link } from '@/router' @@ -6,6 +6,7 @@ import { SectionContainer } from '@/components/layout/containers' import { useTranslation } from 'react-i18next' import { PurposeQueries } from '@/api/purpose' import { useSuspenseQuery } from '@tanstack/react-query' +import { useGetConsumerPurposeGeneralInfoAlertProps } from '../hooks/useGetConsumerPurposeGeneralInfoAlertProps' type ConsumerPurposeSummaryGeneralInformationAccordionProps = { purposeId: string @@ -16,6 +17,7 @@ export const ConsumerPurposeSummaryGeneralInformationAccordion: React.FC< > = ({ purposeId }) => { const { data: purpose } = useSuspenseQuery(PurposeQueries.getSingle(purposeId)) const { t } = useTranslation('purpose', { keyPrefix: 'summary.generalInformationSection' }) + const generalInfoAlertProps = useGetConsumerPurposeGeneralInfoAlertProps(purpose) return ( @@ -36,7 +38,6 @@ export const ConsumerPurposeSummaryGeneralInformationAccordion: React.FC< > {t('eservice.value', { name: purpose.eservice.name, - version: purpose.eservice.descriptor.version, })} } @@ -71,6 +72,7 @@ export const ConsumerPurposeSummaryGeneralInformationAccordion: React.FC< direction="row" label={t('loadEstimationSection.dailyCallsTotal.label')} /> + diff --git a/src/pages/ConsumerPurposeSummaryPage/components/ConsumerPurposeSummaryRiskAnalysisAlertContainer.tsx b/src/pages/ConsumerPurposeSummaryPage/components/ConsumerPurposeSummaryRiskAnalysisAlertContainer.tsx new file mode 100644 index 000000000..d5fc1f366 --- /dev/null +++ b/src/pages/ConsumerPurposeSummaryPage/components/ConsumerPurposeSummaryRiskAnalysisAlertContainer.tsx @@ -0,0 +1,56 @@ +import { getDaysToExpiration, getFormattedExpirationDate } from '@/utils/purpose.utils' +import { Alert, Button, Stack, Typography } from '@mui/material' +import { useNavigate } from '@/router' +import React from 'react' +import { Trans, useTranslation } from 'react-i18next' + +type ConsumerPurposeSummaryRiskAnalysisAlertContainerProps = { + expirationDate?: string + isRulesetExpired: boolean +} + +export const ConsumerPurposeSummaryRiskAnalysisAlertContainer: React.FC< + ConsumerPurposeSummaryRiskAnalysisAlertContainerProps +> = ({ expirationDate, isRulesetExpired }) => { + const { t } = useTranslation('purpose') + + const navigate = useNavigate() + + const daysToExpiration = getDaysToExpiration(expirationDate) + + return ( + <> + {expirationDate && !isRulesetExpired && ( + + , + }} + > + {t('summary.alerts.infoRulesetExpiration', { + days: daysToExpiration, + date: getFormattedExpirationDate(expirationDate), + })} + + + )} + {isRulesetExpired && ( + + + {' '} + {/**TODO FIX SPACING */} + {t('summary.alerts.rulesetExpired.label')} + + + + )} + + ) +} diff --git a/src/pages/ConsumerPurposeSummaryPage/hooks/__tests__/useGetConsumerPurposeGeneralInfoAlertProps.test.ts b/src/pages/ConsumerPurposeSummaryPage/hooks/__tests__/useGetConsumerPurposeGeneralInfoAlertProps.test.ts new file mode 100644 index 000000000..da2223bfc --- /dev/null +++ b/src/pages/ConsumerPurposeSummaryPage/hooks/__tests__/useGetConsumerPurposeGeneralInfoAlertProps.test.ts @@ -0,0 +1,44 @@ +import { + createMockPurposeCallsPerConsumerExceed, + createMockPurposeCallsTotalExceed, + createMockPurposeCallsWithoutExceed, +} from '../../../../../__mocks__/data/purpose.mocks' +import { renderHook } from '@testing-library/react' +import { useGetConsumerPurposeGeneralInfoAlertProps } from '../useGetConsumerPurposeGeneralInfoAlertProps' + +describe('useGetConsumerPurposeInfoAlertProps', () => { + it('should return infoApprovalMayBeRequired if the purpose is undefined', () => { + const { result } = renderHook(() => useGetConsumerPurposeGeneralInfoAlertProps(undefined)) + expect(result.current).toStrictEqual({ + children: 'infoApprovalMayBeRequired', + severity: 'info', + }) + }) + it('should return isDailyCallsTotalExceed if dailyCalls > dailyCallsTotal', () => { + const { result } = renderHook(() => + useGetConsumerPurposeGeneralInfoAlertProps(createMockPurposeCallsTotalExceed()) + ) + expect(result.current).toStrictEqual({ + children: 'infoDailyCallsTotalExceed', + severity: 'info', + }) + }) + it('should return infoDailyCallsPerConsumerExceed if dailyCalls > dailyCallsPerConsumer', () => { + const { result } = renderHook(() => + useGetConsumerPurposeGeneralInfoAlertProps(createMockPurposeCallsPerConsumerExceed()) + ) + expect(result.current).toStrictEqual({ + children: 'infoDailyCallsPerConsumerExceed', + severity: 'info', + }) + }) + it('should return infoApprovalMayBeRequired if no daily calls exceed', () => { + const { result } = renderHook(() => + useGetConsumerPurposeGeneralInfoAlertProps(createMockPurposeCallsWithoutExceed()) + ) + expect(result.current).toStrictEqual({ + children: 'infoApprovalMayBeRequired', + severity: 'info', + }) + }) +}) diff --git a/src/pages/ConsumerPurposeSummaryPage/hooks/useGetConsumerPurposeGeneralInfoAlertProps.ts b/src/pages/ConsumerPurposeSummaryPage/hooks/useGetConsumerPurposeGeneralInfoAlertProps.ts new file mode 100644 index 000000000..1e267c30f --- /dev/null +++ b/src/pages/ConsumerPurposeSummaryPage/hooks/useGetConsumerPurposeGeneralInfoAlertProps.ts @@ -0,0 +1,39 @@ +import type { Purpose } from '@/api/api.generatedTypes' +import type { AlertProps } from '@mui/material' +import { useTranslation } from 'react-i18next' +import { match } from 'ts-pattern' + +export function useGetConsumerPurposeGeneralInfoAlertProps( + purpose: Purpose | undefined +): AlertProps { + const { t } = useTranslation('purpose', { keyPrefix: 'summary.alerts' }) + + if (!purpose?.currentVersion?.dailyCalls) { + return { + severity: 'info', + children: t('infoApprovalMayBeRequired'), + } + } + + const dailyCalls = purpose.currentVersion.dailyCalls + const dailyCallsPerConsumer = purpose.dailyCallsPerConsumer + const dailyCallsTotal = purpose.dailyCallsTotal + + return match({ + isDailyCallsPerConsumerExceed: dailyCalls > dailyCallsPerConsumer, + isDailyCallsTotalExceed: dailyCalls > dailyCallsTotal, + }) + .returnType() + .with({ isDailyCallsTotalExceed: true }, () => ({ + severity: 'info', + children: t('infoDailyCallsTotalExceed'), + })) + .with({ isDailyCallsPerConsumerExceed: true }, () => ({ + severity: 'info', + children: t('infoDailyCallsPerConsumerExceed'), + })) + .otherwise(() => ({ + severity: 'info', + children: t('infoApprovalMayBeRequired'), + })) +} diff --git a/src/pages/ProviderEServiceListPage/ProviderEServiceList.page.tsx b/src/pages/ProviderEServiceListPage/ProviderEServiceList.page.tsx index b8aefcff7..8c03282e3 100644 --- a/src/pages/ProviderEServiceListPage/ProviderEServiceList.page.tsx +++ b/src/pages/ProviderEServiceListPage/ProviderEServiceList.page.tsx @@ -13,12 +13,12 @@ import { } from '@pagopa/interop-fe-commons' import type { GetProducerEServicesParams } from '@/api/api.generatedTypes' import { AuthHooks } from '@/api/auth' -import PlusOneIcon from '@mui/icons-material/PlusOne' import UploadFileIcon from '@mui/icons-material/UploadFile' import type { ActionItemButton } from '@/types/common.types' import { useDrawerState } from '@/hooks/useDrawerState' import { ProviderEServiceImportVersionDrawer } from './components/ProviderEServiceImportVersionDrawer' import { keepPreviousData, useQuery } from '@tanstack/react-query' +import FiberNew from '@mui/icons-material/FiberNew' const ProviderEServiceListPage: React.FC = () => { const { t } = useTranslation('pages', { keyPrefix: 'providerEServiceList' }) @@ -69,7 +69,7 @@ const ProviderEServiceListPage: React.FC = () => { action: () => navigate('PROVIDE_ESERVICE_CREATE'), label: tCommon('createNewBtn'), variant: 'contained', - icon: PlusOneIcon, + icon: FiberNew, }, ] diff --git a/src/pages/ProviderEServiceListPage/components/EServiceTableRow.tsx b/src/pages/ProviderEServiceListPage/components/EServiceTableRow.tsx index 57e32eb8e..3ac698faa 100644 --- a/src/pages/ProviderEServiceListPage/components/EServiceTableRow.tsx +++ b/src/pages/ProviderEServiceListPage/components/EServiceTableRow.tsx @@ -11,8 +11,8 @@ import { TableRow } from '@pagopa/interop-fe-commons' import type { EServiceDescriptorState, ProducerEService } from '@/api/api.generatedTypes' import { AuthHooks } from '@/api/auth' import { useQuery, useQueryClient } from '@tanstack/react-query' -import { ByDelegationChip } from '@/components/shared/ByDelegationChip' import { NotificationBadgeDot } from '@/components/shared/NotificationBadgeDot/NotificationBadgeDot' +import { DelegationTooltip } from '@/components/shared/DelegationTooltip' type EServiceTableRow = { eservice: ProducerEService @@ -69,10 +69,10 @@ export const EServiceTableRow: React.FC = ({ eservice }) => { + {eservice.hasUnreadNotifications && } {eservice.name} - + {eservice.delegation && } ) : ( diff --git a/src/static/locales/en/eservice.json b/src/static/locales/en/eservice.json index 2a014c5a7..d89f92abb 100644 --- a/src/static/locales/en/eservice.json +++ b/src/static/locales/en/eservice.json @@ -5,10 +5,10 @@ "emptyTitle": "Create e-service", "stepper": { "step1Label": "General", - "step2Label": "Version", + "step2Label": "Thresholds and attributes", "step2ReceiveLabel": "Purpose", - "step3Label": "Attributes", - "step4Label": "Documentation" + "step3Label": "Technical specifications", + "step4Label": "Additional info" }, "backWithoutSaveBtn": "Back", "forwardWithSaveBtn": "Save draft and proceed", diff --git a/src/static/locales/en/purpose.json b/src/static/locales/en/purpose.json index 9c47d778c..9d67a83c6 100644 --- a/src/static/locales/en/purpose.json +++ b/src/static/locales/en/purpose.json @@ -2,7 +2,8 @@ "backToListBtn": "Back to purposes", "create": { "emptyTitle": "Create purpose", - "preliminaryInformationSectionTitle": "Preliminary information", + "requiredLabel": "*Required fields.", + "preliminaryInformationSectionTitle": "General informations", "isTemplateField": { "label": "Clone from template" }, @@ -13,11 +14,11 @@ }, "consumerField": { "label": "Consumer", - "infoLabel": "Among the members, you can only select your organization or the members for whom you have an active delegation" + "infoLabel": "Select your organization or the organization for which you have an active delegation as a consumer" }, "eserviceField": { "label": "Choose e-service", - "infoLabel": "You can only select the e-services for which the selected user has an active request for use", + "infoLabel": "Here you will find only the e-services for which the consumer has an active request", "alert": { "label": "The facilitated compilation feature is available for the selected e-service!" } @@ -44,7 +45,7 @@ "riskAnalysisPreviewTitle": "Risk analysis", "purposeTemplateField": { "title": "Facilitated purpose compilation", - "description": "The facilitated compilation feature provides annotations, supporting documents, and some pre-filled answers to help you formulate the risk analysis. The template is made available for reuse by other participating organizations and can be used without restrictions.", + "description": "This feature guides you through the risk analysis with annotations, supporting documents, and some pre-populated responses. The simplified template is available for reuse by other producers and can be used without restrictions.", "usePurposeTemplateSwitch": { "label": "Use the facilitated compilation feature", "disabledTooltip": "It is not possible to use the feature for the selected \n e-service because the producer has not indicated how personal data will be processed", @@ -74,26 +75,47 @@ "stepGeneral": { "title": "General information", "nameField": { - "label": "Purpose name (required)", + "label": "Purpose name", "infoLabel": "It will help you tell them apart. Min 5 characters, max 60 characters" }, "descriptionField": { - "label": "Purpose description (required)", + "label": "Purpose description", "infoLabel": "Min 10 characters, max 250 characters" }, "isFreeOfChargeField": { - "label": "Indicate whether access to the data made available with the use of this e-service is free of charge (required)", + "label": "How will you provide access to data from this e-service?*", "options": { - "YES": "Yes", - "NO": "No" + "YES": "Free of charge", + "NO": "For a fee" } }, "freeOfChargeReasonField": { "label": "Free of charge reason (required)", - "infoLabel": "It is requested to specify whether the provision of data is free of charge by virtue of an exemption/exclusion provided for by law or other. Min 10 characters, max 250 characters" + "infoLabel": "Explain why you will make this data available free of charge. Indicate whether you are doing so under a statutory exemption or exclusion, or for another reason. Minimum 10 characters, maximum 250 characters." + } + }, + "loadEstimationSection": { + "title": "Estimate API calls", + "description": "Indicates the number of calls expected each day for this purpose.", + "dailyCalls": { + "label": "Estimated number of API calls/day", + "infoLabel": "Please enter a number greater than or equal to 1." + }, + "providerThresholdsInfo": { + "label": "Thresholds defined by the provider", + "description": "Residual calls may vary if the provider receives other purposes before your publication.", + "dailyCallsPerConsumer": { + "label": "Threshold per user", + "value": "{{min}} of {{max}} API calls/day" + }, + "dailyCallsTotal": { + "label": "Total threshold", + "value": "{{min}} of {{max}} API calls/day" + } }, - "dailyCallsField": { - "label": "How many API calls do you expect to make per day?" + "alerts": { + "infoDailyCallsPerConsumerExceed": "The number you entered exceeds the user limit. The purpose must be approved by the provider.", + "infoDailyCallsTotalExceed": "The number you entered exceeds the total threshold. The purpose must be approved by the provider." } }, "stepRiskAnalysis": { @@ -169,13 +191,16 @@ "label": "This risk analysis uses an obsolete version that you cannot publish.", "action": "Create new purpose" }, - "infoRulesetExpiration": "This risk analysis uses a version that will become obsolete in {{days}} days. Publish it by {{date}} to avoid having to recompile it." + "infoRulesetExpiration": "This risk analysis uses a version that will become obsolete in {{days}} days. Publish it by {{date}} to avoid having to recompile it.", + "infoApprovalMayBeRequired": "If the producer receives other purposes before your publication, approval may be required.", + "infoDailyCallsPerConsumerExceed": "The estimated number of calls you indicated exceeds the per-user threshold. The purpose must be approved by the producer.", + "infoDailyCallsTotalExceed": "The estimated number of calls you indicated exceeds the total threshold. The purpose will need to be approved by the producer." }, "generalInformationSection": { "title": "General information", "eservice": { "label": "Reference e-service", - "value": "{{name}}, version {{version}}" + "value": "{{name}}" }, "producer": { "label": "Producer" diff --git a/src/static/locales/en/purposeTemplate.json b/src/static/locales/en/purposeTemplate.json index 31941193e..0a6418f0c 100644 --- a/src/static/locales/en/purposeTemplate.json +++ b/src/static/locales/en/purposeTemplate.json @@ -43,7 +43,7 @@ "freeOfChargeReason": "I'm a Public Administration" }, "stepper": { - "step1Label": "General", + "step1Label": "General informations", "step2Label": "Suggested e-services", "step3Label": "Risk analysis" }, diff --git a/src/static/locales/en/shared-components.json b/src/static/locales/en/shared-components.json index 2f86027cd..59b072516 100644 --- a/src/static/locales/en/shared-components.json +++ b/src/static/locales/en/shared-components.json @@ -371,6 +371,13 @@ "delegate": "Received by Delegation" } }, + "delegationTooltip": { + "label": { + "default": "By Delegation", + "delegator": "Delegated to {{delegate}}", + "delegate": "Received by Delegation by {{delegator}}" + } + }, "riskAnalysis": { "formComponents": { "emptyLabel": "No values available", @@ -472,9 +479,9 @@ "SUBSCRIBE_CATALOG_LIST": "E-service Catalog", "SUBSCRIBE_PURPOSE_CREATE": "Create Purpose", "SUBSCRIBE_PURPOSE_EDIT": "Edit Purpose", - "SUBSCRIBE_PURPOSE_SUMMARY": "Purpose Summary", + "SUBSCRIBE_PURPOSE_SUMMARY": "Create Purpose", "SUBSCRIBE_PURPOSE_DETAILS": "Manage Purpose", - "SUBSCRIBE_PURPOSE_LIST": "Submitted Purposes", + "SUBSCRIBE_PURPOSE_LIST": "Your Purposes", "SUBSCRIBE_CLIENT_OPERATOR_EDIT": "Manage E-service Client Operator", "SUBSCRIBE_CLIENT_KEY_EDIT": "Manage E-service Client Public Key", "SUBSCRIBE_CLIENT_CREATE": "Create E-service Client", diff --git a/src/static/locales/it/eservice.json b/src/static/locales/it/eservice.json index b0a68b903..3c5e30625 100644 --- a/src/static/locales/it/eservice.json +++ b/src/static/locales/it/eservice.json @@ -5,10 +5,10 @@ "emptyTitle": "Crea e-service", "stepper": { "step1Label": "Generale", - "step2Label": "Versione", + "step2Label": "Soglie e attributi", "step2ReceiveLabel": "Finalità", - "step3Label": "Attributi", - "step4Label": "Documentazione" + "step3Label": "Specifiche tecniche", + "step4Label": "Info aggiuntive" }, "backWithoutSaveBtn": "Indietro", "forwardWithSaveBtn": "Salva bozza e prosegui", diff --git a/src/static/locales/it/purpose.json b/src/static/locales/it/purpose.json index 606c534f9..f8de9c879 100644 --- a/src/static/locales/it/purpose.json +++ b/src/static/locales/it/purpose.json @@ -2,7 +2,8 @@ "backToListBtn": "Torna alle finalità", "create": { "emptyTitle": "Crea finalità", - "preliminaryInformationSectionTitle": "Informazioni preliminari", + "requiredLabel": "*Campi obbligatori.", + "preliminaryInformationSectionTitle": "Informazioni generali", "isTemplateField": { "label": "Usa un template precompilato" }, @@ -13,11 +14,11 @@ }, "consumerField": { "label": "Fruitore", - "infoLabel": "Tra gli aderenti, puoi selezionare solo il tuo ente o gli aderenti per i quali hai una delega in fruizione attiva" + "infoLabel": "Seleziona il tuo ente o l'ente per cui hai una delega attiva come fruitore" }, "eserviceField": { "label": "E-service da associare", - "infoLabel": "Puoi selezionare solo gli e-service per i quali il fruitore selezionato ha una richiesta di fruizione attiva", + "infoLabel": "Qui trovi solo gli e-service per cui il fruitore ha una richiesta attiva", "alert": { "label": "Per l’e-service selezionato è possibile utilizzare la funzionalità di compilazione agevolata!" } @@ -44,9 +45,9 @@ "riskAnalysisPreviewTitle": "Analisi del rischio", "purposeTemplateField": { "title": "Compilazione agevolata della finalità", - "description": "La funzionalità di compilazione agevolata mette a disposizione annotazioni, documenti di supporto e alcune risposte precompilate per facilitare la formulazione dell’analisi del rischio. Il modello è reso disponibile per il riuso da altri enti aderenti e può essere utilizzato senza vincoli.", + "description": "Questa funzionalità ti guida durante l'analisi del rischio con annotazioni, documenti di supporto e alcune risposte precompilate. Il modello agevolato è reso disponibile per il riuso da altri enti e può essere usato senza vincoli.", "usePurposeTemplateSwitch": { - "label": "Utilizza la funzionalità di compilazione agevolata", + "label": "Usa la compilazione agevolata", "disabledTooltip": "Non è possibile utilizzare la funzionalità per \n l’e-service selezionato in quanto l’erogatore non ha indicato come verranno trattati i dati personali", "selectPurposeTemplate": { "title": "Scegli il modello di compilazione agevolata della finalità", @@ -64,7 +65,7 @@ "edit": { "emptyTitle": "Modifica finalità", "stepper": { - "stepGeneralLabel": "Generale", + "stepGeneralLabel": "Informazioni generali", "stepRiskAnalysisLabel": "Analisi del rischio" }, "backWithoutSaveBtn": "Indietro", @@ -74,26 +75,47 @@ "stepGeneral": { "title": "Informazioni generali", "nameField": { - "label": "Nome della finalità (richiesto)", + "label": "Nome della finalità", "infoLabel": "Ti aiuterà a distinguerla dalle altre. Min 5 caratteri, max 60 caratteri" }, "descriptionField": { - "label": "Descrizione della finalità (richiesto)", + "label": "Descrizione finalità", "infoLabel": "Min 10 caratteri, max 250 caratteri" }, "isFreeOfChargeField": { - "label": "Indicare se l’accesso ai dati messi a disposizione con la fruizione del presente E-service è a titolo gratuito (richiesto)", + "label": "Come metterai a disposizione l'accesso ai dati di questo e-service?*", "options": { - "YES": "Sì", - "NO": "No" + "YES": "A titolo gratuito", + "NO": "A pagamento" } }, "freeOfChargeReasonField": { "label": "Motivazione titolo gratuito (richiesto)", - "infoLabel": "Si richiede di specificare se la messa a disposizione dei dati è a titolo gratuito in forza di una esenzione/esclusione prevista dalla legge o altro. Min 10 caratteri, max 250 caratteri" + "infoLabel": "Spiega perché metterai questi dati a disposizione gratuitamente. Indica se lo fai in base a un’esenzione o a un’esclusione prevista dalla legge, oppure per un altro motivo. Min 10, max 250 caratteri" + } + }, + "loadEstimationSection": { + "title": "Stima chiamate API", + "description": "Indica il numero di chiamate previste ogni giorno per questa finalità.", + "dailyCalls": { + "label": "Stima numero chiamate API/giorno", + "infoLabel": "Inserisci un numero superiore o uguale a 1" + }, + "providerThresholdsInfo": { + "label": "Soglie definite dall’erogatore", + "description": "Le chiamate residue possono variare se l'erogatore riceve altre finalità prima della tua pubblicazione.", + "dailyCallsPerConsumer": { + "label": "Soglia per fruitore", + "value": "{{min}} di {{max}} chiamate API/giorno" + }, + "dailyCallsTotal": { + "label": "Soglia totale", + "value": "{{min}} di {{max}} chiamate API/giorno" + } }, - "dailyCallsField": { - "label": "Quante chiamate API/giorno stimi di effettuare?" + "alerts": { + "infoDailyCallsPerConsumerExceed": "Il numero che hai inserito supera la soglia prevista per fruitore. La finalità dovrà essere approvata dall’erogatore.", + "infoDailyCallsTotalExceed": "Il numero che hai inserito supera la soglia totale. La finalità dovrà essere approvata dall’erogatore." } }, "stepRiskAnalysis": { @@ -169,13 +191,16 @@ "label": "Questa analisi del rischio utilizza una versione obsoleta che non puoi pubblicare.", "action": "Crea nuova finalità" }, - "infoRulesetExpiration": "Questa analisi del rischio utilizza una versione che sarà obsoleta tra {{days}} giorni. Pubblicala entro {{date}} per non doverla ricompilare." + "infoRulesetExpiration": "Questa analisi del rischio utilizza una versione che sarà obsoleta tra {{days}} giorni. Pubblicala entro {{date}} per non doverla ricompilare.", + "infoApprovalMayBeRequired": "Se l’erogatore riceve altre finalità prima della tua pubblicazione, potrebbe essere necessaria l’approvazione.", + "infoDailyCallsPerConsumerExceed": "La stima di chiamate che hai indicato supera la soglia per fruitore. La finalità dovrà essere approvata dall’erogatore.", + "infoDailyCallsTotalExceed": "La stima di chiamate che hai indicato supera la soglia totale. La finalità dovrà essere approvata dall’erogatore." }, "generalInformationSection": { "title": "Informazioni generali", "eservice": { - "label": "E-service di riferimento", - "value": "{{name}}, versione {{version}}" + "label": "E-service associato", + "value": "{{name}}" }, "producer": { "label": "Erogatore" @@ -184,17 +209,17 @@ "label": "Descrizione" }, "loadEstimationSection": { - "title": "Stima di carico", + "title": "Stima e soglie di chiamate API", "dailyCalls": { - "label": "Il tuo piano richiesto", + "label": "Stima richiesta dal fruitore", "value": "{{value}} chiamate API/giorno" }, "dailyCallsPerConsumer": { - "label": "Soglia per fruitore imposta dall'erogatore", + "label": "Soglia per fruitore prevista dall'erogatore", "value": "{{value}} chiamate API/giorno" }, "dailyCallsTotal": { - "label": "Soglia totale imposta dall'erogatore", + "label": "Soglia totale prevista dall'erogatore", "value": "{{value}} chiamate API/giorno" } } diff --git a/src/static/locales/it/purposeTemplate.json b/src/static/locales/it/purposeTemplate.json index 9dac32a01..342eebc5f 100644 --- a/src/static/locales/it/purposeTemplate.json +++ b/src/static/locales/it/purposeTemplate.json @@ -43,7 +43,7 @@ "freeOfChargeReason": "Sono una Pubblica Amministrazione" }, "stepper": { - "step1Label": "Generale", + "step1Label": "Informazioni generali", "step2Label": "E-service suggeriti", "step3Label": "Analisi del rischio" }, diff --git a/src/static/locales/it/shared-components.json b/src/static/locales/it/shared-components.json index e11605455..c273d819e 100644 --- a/src/static/locales/it/shared-components.json +++ b/src/static/locales/it/shared-components.json @@ -371,6 +371,13 @@ "delegate": "Ricevuto in delega" } }, + "delegationTooltip": { + "label": { + "default": "In delega", + "delegator": "Delegato a {{delegate}}", + "delegate": "Ricevuto in delega da {{delegator}}" + } + }, "riskAnalysis": { "formComponents": { "emptyLabel": "Nessun valore disponibile", @@ -472,9 +479,9 @@ "SUBSCRIBE_CATALOG_LIST": "Catalogo e-service", "SUBSCRIBE_PURPOSE_CREATE": "Crea finalità", "SUBSCRIBE_PURPOSE_EDIT": "Modifica finalità", - "SUBSCRIBE_PURPOSE_SUMMARY": "Riepilogo finalità", + "SUBSCRIBE_PURPOSE_SUMMARY": "Crea finalità", "SUBSCRIBE_PURPOSE_DETAILS": "Gestisci singola finalità", - "SUBSCRIBE_PURPOSE_LIST": "Finalità inoltrate", + "SUBSCRIBE_PURPOSE_LIST": "Le tue finalità", "SUBSCRIBE_CLIENT_OPERATOR_EDIT": "Gestisci operatore del client e-service", "SUBSCRIBE_CLIENT_KEY_EDIT": "Gestisci chiave pubblica del client e-service", "SUBSCRIBE_CLIENT_CREATE": "Crea client e-service",