diff --git a/scripts/open-api-type-generator.js b/scripts/open-api-type-generator.js index bddc27c5e..c9ffa9873 100644 --- a/scripts/open-api-type-generator.js +++ b/scripts/open-api-type-generator.js @@ -2,7 +2,7 @@ import { generateApi } from 'swagger-typescript-api' import path from 'path' const openApiSpecificationFileUrl = - 'https://raw.githubusercontent.com/pagopa/interop-be-monorepo/refs/heads/main/packages/api-clients/open-api/bffApi.yml' + 'https://raw.githubusercontent.com/pagopa/interop-be-monorepo/refs/heads/develop/packages/api-clients/open-api/bffApi.yml' const apiFolderPath = path.resolve('./src/api/') diff --git a/src/api/api.generatedTypes.ts b/src/api/api.generatedTypes.ts index 03da2e72f..4ee5c3bab 100644 --- a/src/api/api.generatedTypes.ts +++ b/src/api/api.generatedTypes.ts @@ -181,6 +181,10 @@ export interface EServiceDescriptionSeed { description: string } +export interface RejectDelegatedEServiceDescriptorSeed { + rejectionReason: string +} + export interface CatalogEServiceDescriptor { /** @format uuid */ id: string @@ -331,6 +335,7 @@ export interface ProducerEServiceDescriptor { agreementApprovalPolicy: AgreementApprovalPolicy eservice: ProducerDescriptorEService attributes: DescriptorAttributes + rejectionReasons?: DescriptorRejectionReason[] } export interface ProducerDescriptorEService { @@ -361,6 +366,12 @@ export interface UpdateEServiceDescriptorDocumentSeed { prettyName: string } +export interface DescriptorRejectionReason { + rejectionReason: string + /** @format date-time */ + rejectedAt: string +} + /** * EService Descriptor policy for new Agreements approval. * AUTOMATIC - the agreement will be automatically approved if Consumer attributes are met @@ -612,14 +623,24 @@ export interface PresignedUrl { url: string } +export interface CompactProducerDescriptor { + /** @format uuid */ + id: string + /** EService Descriptor State */ + state: EServiceDescriptorState + version: string + audience: string[] + requireCorrections?: boolean +} + export interface ProducerEService { /** @format uuid */ id: string name: string /** Risk Analysis Mode */ mode: EServiceMode - activeDescriptor?: CompactDescriptor - draftDescriptor?: CompactDescriptor + activeDescriptor?: CompactProducerDescriptor + draftDescriptor?: CompactProducerDescriptor } export interface ProducerEServices { @@ -627,13 +648,6 @@ export interface ProducerEServices { pagination: Pagination } -export interface ProductInfo { - id: string - role: string - /** @format date-time */ - createdAt: string -} - export interface SelfcareProduct { id: string name: string @@ -926,6 +940,7 @@ export type EServiceDescriptorState = | 'DEPRECATED' | 'SUSPENDED' | 'ARCHIVED' + | 'WAITING_FOR_APPROVAL' /** EService Descriptor State */ export type EServiceTechnology = 'REST' | 'SOAP' @@ -1107,16 +1122,29 @@ export interface Tenants { pagination: Pagination } -export interface TenantFeature { - /** Certifier Tenant Feature */ - certifier?: Certifier -} +export type TenantFeatureType = 'PERSISTENT_CERTIFIER' | 'DELEGATED_PRODUCER' + +export type TenantFeature = + | { + /** Certifier Tenant Feature */ + certifier?: Certifier + } + | { + /** Delegated producer Tenant Feature */ + delegatedProducer?: DelegatedProducer + } /** Certifier Tenant Feature */ export interface Certifier { certifierId: string } +/** Delegated producer Tenant Feature */ +export interface DelegatedProducer { + /** @format date-time */ + availabilityTimestamp: string +} + export interface CompactTenant { /** @format uuid */ id: string @@ -1176,6 +1204,8 @@ export interface UpdateVerifiedTenantAttributeSeed { export interface VerifiedTenantAttributeSeed { /** @format uuid */ id: string + /** @format uuid */ + agreementId: string /** @format date-time */ expirationDate?: string } @@ -1211,6 +1241,8 @@ export interface TenantVerifier { expirationDate?: string /** @format date-time */ extensionDate?: string + /** @format uuid */ + delegationId?: string } export interface TenantRevoker { @@ -1224,6 +1256,8 @@ export interface TenantRevoker { extensionDate?: string /** @format date-time */ revocationDate: string + /** @format uuid */ + delegationId?: string } export interface TokenGenerationValidationResult { @@ -1289,6 +1323,74 @@ export interface CertifiedTenantAttributeSeed { id: string } +/** Delegation State */ +export type DelegationKind = 'DELEGATED_PRODUCER' | 'DELEGATED_CONSUMER' + +/** Delegation State */ +export type DelegationState = 'WAITING_FOR_APPROVAL' | 'ACTIVE' | 'REJECTED' | 'REVOKED' + +export interface DelegationTenant { + /** @format uuid */ + id: string + name: string +} + +export interface DelegationEService { + /** @format uuid */ + id: string + name: string + description?: string + /** @format uuid */ + producerId: string + producerName: string + descriptors: CompactDescriptor[] +} + +export interface Delegation { + /** @format uuid */ + id: string + eservice: DelegationEService + delegate: DelegationTenant + delegator: DelegationTenant + activationContract?: Document + revocationContract?: Document + /** @format date-time */ + submittedAt?: string + rejectionReason?: string + /** Delegation State */ + state: DelegationState + /** Delegation State */ + kind: DelegationKind +} + +export interface CompactDelegation { + /** @format uuid */ + id: string + eservice?: CompactEServiceLight + delegate: DelegationTenant + delegator: DelegationTenant + /** Delegation State */ + state: DelegationState + /** Delegation State */ + kind: DelegationKind +} + +export interface CompactDelegations { + results: CompactDelegation[] + pagination: Pagination +} + +export interface DelegationSeed { + /** @format uuid */ + eserviceId: string + /** @format uuid */ + delegateId: string +} + +export interface RejectDelegationPayload { + rejectionReason: string +} + export interface Problem { /** URI reference of type definition */ type: string @@ -1499,6 +1601,8 @@ export interface GetProducerEServicesParams { * @default [] */ consumersIds?: string[] + /** if true only delegated e-services will be returned, if false only non-delegated e-services will be returned, if not present all e-services will be returned */ + delegated?: boolean /** * @format int32 * @min 0 @@ -1653,6 +1757,11 @@ export interface GetConsumerPurposesParams { limit: number } +export interface RevokeVerifiedAttributePayload { + /** @format uuid */ + agreementId: string +} + export interface GetAttributesParams { /** Query to filter Attributes by name */ q?: string @@ -1669,6 +1778,11 @@ export interface GetAttributesParams { export interface GetTenantsParams { name?: string + /** + * comma separated feature types to filter the teanants with + * @default [] + */ + features?: TenantFeatureType[] /** * @format int32 * @min 1 @@ -1773,6 +1887,39 @@ export interface GetProducerKeysParams { producerKeychainId: string } +export interface GetDelegationsParams { + /** + * @format int32 + * @min 0 + */ + offset: number + /** + * @format int32 + * @min 1 + * @max 50 + */ + limit: number + /** + * comma separated sequence of delegation states to filter the results with + * @default [] + */ + states?: DelegationState[] + /** + * The delegator ids to filter by + * @default [] + */ + delegatorIds?: string[] + /** + * The delegated ids to filter by + * @default [] + */ + delegateIds?: string[] + /** The delegation kind to filter by */ + kind?: DelegationKind + /** @default [] */ + eserviceIds?: string[] +} + export namespace Agreements { /** * @description retrieves a list of agreements @@ -2924,6 +3071,62 @@ export namespace Eservices { } export type ResponseBody = CreatedResource } + /** + * No description + * @tags eservices + * @name ApproveDelegatedEServiceDescriptor + * @summary approve a delegated new e-service version + * @request POST:/eservices/{eServiceId}/descriptors/{descriptorId}/approve + * @secure + */ + export namespace ApproveDelegatedEServiceDescriptor { + export type RequestParams = { + /** + * the eservice id + * @format uuid + */ + eServiceId: string + /** + * the descriptor id + * @format uuid + */ + descriptorId: string + } + export type RequestQuery = {} + export type RequestBody = never + export type RequestHeaders = { + 'X-Correlation-Id': string + } + export type ResponseBody = CreatedResource + } + /** + * No description + * @tags eservices + * @name RejectDelegatedEServiceDescriptor + * @summary reject a delegated new e-service version + * @request POST:/eservices/{eServiceId}/descriptors/{descriptorId}/reject + * @secure + */ + export namespace RejectDelegatedEServiceDescriptor { + export type RequestParams = { + /** + * the eservice id + * @format uuid + */ + eServiceId: string + /** + * the descriptor id + * @format uuid + */ + descriptorId: string + } + export type RequestQuery = {} + export type RequestBody = RejectDelegatedEServiceDescriptorSeed + export type RequestHeaders = { + 'X-Correlation-Id': string + } + export type ResponseBody = CreatedResource + } } export namespace Export { @@ -3031,6 +3234,8 @@ export namespace Producers { * @default [] */ consumersIds?: string[] + /** if true only delegated e-services will be returned, if false only non-delegated e-services will be returned, if not present all e-services will be returned */ + delegated?: boolean /** * @format int32 * @min 0 @@ -3500,7 +3705,7 @@ export namespace Tenants { attributeId: string } export type RequestQuery = {} - export type RequestBody = never + export type RequestBody = RevokeVerifiedAttributePayload export type RequestHeaders = {} export type ResponseBody = void } @@ -3586,6 +3791,11 @@ export namespace Tenants { export type RequestParams = {} export type RequestQuery = { name?: string + /** + * comma separated feature types to filter the teanants with + * @default [] + */ + features?: TenantFeatureType[] /** * @format int32 * @min 1 @@ -3599,6 +3809,36 @@ export namespace Tenants { } export type ResponseBody = Tenants } + /** + * No description + * @tags tenants + * @name AssignTenantDelegatedProducerFeature + * @summary Assign delegated producer feature to tenant caller + * @request POST:/tenants/delegatedProducer + * @secure + */ + export namespace AssignTenantDelegatedProducerFeature { + export type RequestParams = {} + export type RequestQuery = {} + export type RequestBody = never + export type RequestHeaders = {} + export type ResponseBody = void + } + /** + * No description + * @tags tenants + * @name DeleteTenantDelegatedProducerFeature + * @summary Delete delegated producer feature to tenant caller + * @request DELETE:/tenants/delegatedProducer + * @secure + */ + export namespace DeleteTenantDelegatedProducerFeature { + export type RequestParams = {} + export type RequestQuery = {} + export type RequestBody = never + export type RequestHeaders = {} + export type ResponseBody = void + } } export namespace Tools { @@ -4002,6 +4242,89 @@ export namespace Producer { } export type ResponseBody = Purposes } + /** + * @description creates the producer delegation + * @tags producerDelegations + * @name CreateProducerDelegation + * @summary Producer delegation creation + * @request POST:/producer/delegations + * @secure + */ + export namespace CreateProducerDelegation { + export type RequestParams = {} + export type RequestQuery = {} + export type RequestBody = DelegationSeed + export type RequestHeaders = { + 'X-Correlation-Id': string + } + export type ResponseBody = CreatedResource + } + /** + * @description Approves a producer delegation + * @tags producerDelegations + * @name ApproveDelegation + * @summary Approves a producer delegation + * @request POST:/producer/delegations/{delegationId}/approve + * @secure + */ + export namespace ApproveDelegation { + export type RequestParams = { + /** + * The identifier of the delegation + * @format uuid + */ + delegationId: string + } + export type RequestQuery = {} + export type RequestBody = never + export type RequestHeaders = { + 'X-Correlation-Id': string + } + export type ResponseBody = void + } + /** + * @description Rejects a producer delegation + * @tags producerDelegations + * @name RejectDelegation + * @summary Rejects a producer delegation + * @request POST:/producer/delegations/{delegationId}/reject + * @secure + */ + export namespace RejectDelegation { + export type RequestParams = { + /** + * The identifier of the delegation + * @format uuid + */ + delegationId: string + } + export type RequestQuery = {} + export type RequestBody = RejectDelegationPayload + export type RequestHeaders = { + 'X-Correlation-Id': string + } + export type ResponseBody = void + } + /** + * @description Revokes a producer delegation + * @tags producerDelegations + * @name RevokeProducerDelegation + * @summary Revokes a producer delegation + * @request DELETE:/producer/delegations/{delegationId} + * @secure + */ + export namespace RevokeProducerDelegation { + export type RequestParams = { + /** The delegation id */ + delegationId: string + } + export type RequestQuery = {} + export type RequestBody = never + export type RequestHeaders = { + 'X-Correlation-Id': string + } + export type ResponseBody = void + } } export namespace Consumer { @@ -5042,6 +5365,99 @@ export namespace ProducerKeychains { } } +export namespace Delegations { + /** + * @description List delegations + * @tags delegations + * @name GetDelegations + * @summary List delegations + * @request GET:/delegations + * @secure + */ + export namespace GetDelegations { + export type RequestParams = {} + export type RequestQuery = { + /** + * @format int32 + * @min 0 + */ + offset: number + /** + * @format int32 + * @min 1 + * @max 50 + */ + limit: number + /** + * comma separated sequence of delegation states to filter the results with + * @default [] + */ + states?: DelegationState[] + /** + * The delegator ids to filter by + * @default [] + */ + delegatorIds?: string[] + /** + * The delegated ids to filter by + * @default [] + */ + delegateIds?: string[] + /** The delegation kind to filter by */ + kind?: DelegationKind + /** @default [] */ + eserviceIds?: string[] + } + export type RequestBody = never + export type RequestHeaders = { + 'X-Correlation-Id': string + } + export type ResponseBody = CompactDelegations + } + /** + * @description Retrieves delegation + * @tags delegations + * @name GetDelegation + * @summary Retrieves delegation + * @request GET:/delegations/{delegationId} + * @secure + */ + export namespace GetDelegation { + export type RequestParams = { + /** The delegation id */ + delegationId: string + } + export type RequestQuery = {} + export type RequestBody = never + export type RequestHeaders = { + 'X-Correlation-Id': string + } + export type ResponseBody = Delegation + } + /** + * @description Retrieve a contract of a delegation + * @tags delegations + * @name GetDelegationContract + * @summary Retrieve a contract of a delegation + * @request GET:/delegations/{delegationId}/contracts/{contractId} + * @secure + */ + export namespace GetDelegationContract { + export type RequestParams = { + /** @format uuid */ + delegationId: string + /** @format uuid */ + contractId: string + } + export type RequestQuery = {} + export type RequestBody = never + export type RequestHeaders = { + 'X-Correlation-Id': string + } + export type ResponseBody = File + } +} + export namespace Status { /** * @description Return ok diff --git a/src/api/delegation/delegation.downloads.ts b/src/api/delegation/delegation.downloads.ts new file mode 100644 index 000000000..2f82350d3 --- /dev/null +++ b/src/api/delegation/delegation.downloads.ts @@ -0,0 +1,17 @@ +import { useTranslation } from 'react-i18next' +import { useDownloadFile } from '../hooks' +import { DelegationServices } from './delegation.services' + +function useDownloadDelegationContract() { + const { t } = useTranslation('mutations-feedback', { + keyPrefix: 'delegation.downloadDelegationContract', + }) + return useDownloadFile(DelegationServices.downloadDelegationContract, { + errorToastLabel: t('outcome.error'), + loadingLabel: t('loading'), + }) +} + +export const DelegationDownloads = { + useDownloadDelegationContract, +} diff --git a/src/api/delegation/delegation.mutations.ts b/src/api/delegation/delegation.mutations.ts new file mode 100644 index 000000000..765181555 --- /dev/null +++ b/src/api/delegation/delegation.mutations.ts @@ -0,0 +1,87 @@ +import { useTranslation } from 'react-i18next' +import { DelegationServices } from './delegation.services' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { DelegationQueries } from './delegation.queries' + +function useCreateProducerDelegation() { + const { t } = useTranslation('mutations-feedback', { + keyPrefix: 'delegation.createProducerDelegation', + }) + return useMutation({ + mutationFn: DelegationServices.createProducerDelegation, + meta: { + errorToastLabel: t('outcome.error'), + loadingLabel: t('loading'), + successToastLabel: t('outcome.success'), + }, + }) +} + +function useApproveProducerDelegation() { + const { t } = useTranslation('mutations-feedback', { + keyPrefix: 'delegation.approveProducerDelegation', + }) + return useMutation({ + mutationFn: DelegationServices.approveProducerDelegation, + meta: { + successToastLabel: t('outcome.success'), + errorToastLabel: t('outcome.error'), + loadingLabel: t('loading'), + }, + }) +} + +function useRejectProducerDelegation() { + const { t } = useTranslation('mutations-feedback', { + keyPrefix: 'delegation.rejectProducerDelegation', + }) + return useMutation({ + mutationFn: DelegationServices.rejectProducerDelegation, + meta: { + successToastLabel: t('outcome.success'), + errorToastLabel: t('outcome.error'), + loadingLabel: t('loading'), + }, + }) +} + +function useRevokeProducerDelegation() { + const { t } = useTranslation('mutations-feedback', { + keyPrefix: 'delegation.revokeProducerDelegation', + }) + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: DelegationServices.revokeProducerDelegation, + meta: { + successToastLabel: t('outcome.success'), + errorToastLabel: t('outcome.error'), + loadingLabel: t('loading'), + }, + onSuccess(_, { delegationId }) { + queryClient.removeQueries(DelegationQueries.getSingle({ delegationId })) + }, + }) +} + +function useCreateProducerDelegationAndEservice() { + const { t } = useTranslation('mutations-feedback', { + keyPrefix: 'delegation.createProducerDelegation', + }) + return useMutation({ + mutationFn: DelegationServices.createProducerDelegationAndEservice, + meta: { + successToastLabel: t('outcome.success'), + errorToastLabel: t('outcome.error'), + loadingLabel: t('loading'), + }, + }) +} + +export const DelegationMutations = { + useCreateProducerDelegation, + useApproveProducerDelegation, + useRejectProducerDelegation, + useRevokeProducerDelegation, + useCreateProducerDelegationAndEservice, +} diff --git a/src/api/delegation/delegation.queries.ts b/src/api/delegation/delegation.queries.ts new file mode 100644 index 000000000..20fd6917a --- /dev/null +++ b/src/api/delegation/delegation.queries.ts @@ -0,0 +1,22 @@ +import { queryOptions } from '@tanstack/react-query' +import type { GetDelegationsParams } from '../api.generatedTypes' +import { DelegationServices } from './delegation.services' + +function getProducerDelegationsList(params: GetDelegationsParams) { + return queryOptions({ + queryKey: ['DelegationGetProducerDelegationsList', params], + queryFn: () => DelegationServices.getProducerDelegations(params), + }) +} + +function getSingle({ delegationId }: { delegationId: string }) { + return queryOptions({ + queryKey: ['DelegationGetSingle', delegationId], + queryFn: () => DelegationServices.getSingle({ delegationId }), + }) +} + +export const DelegationQueries = { + getProducerDelegationsList, + getSingle, +} diff --git a/src/api/delegation/delegation.services.ts b/src/api/delegation/delegation.services.ts new file mode 100644 index 000000000..57cb50161 --- /dev/null +++ b/src/api/delegation/delegation.services.ts @@ -0,0 +1,97 @@ +import axiosInstance from '@/config/axios' +import { waitFor } from '@/utils/common.utils' +import type { + CompactDelegations, + CreatedResource, + Delegation, + DelegationSeed, + EServiceSeed, + GetDelegationsParams, + RejectDelegationPayload, +} from '../api.generatedTypes' +import { BACKEND_FOR_FRONTEND_URL } from '@/config/env' +import { EServiceServices } from '../eservice' + +async function getProducerDelegations(params: GetDelegationsParams) { + const response = await axiosInstance.get( + `${BACKEND_FOR_FRONTEND_URL}/delegations`, + { params } + ) + + return response.data +} + +async function getSingle({ delegationId }: { delegationId: string }) { + const response = await axiosInstance.get( + `${BACKEND_FOR_FRONTEND_URL}/delegations/${delegationId}` + ) + + return response.data +} + +async function createProducerDelegation(payload: DelegationSeed) { + const response = await axiosInstance.post( + `${BACKEND_FOR_FRONTEND_URL}/producer/delegations`, + payload + ) + return response.data +} + +async function approveProducerDelegation({ delegationId }: { delegationId: string }) { + return axiosInstance.post( + `${BACKEND_FOR_FRONTEND_URL}/producer/delegations/${delegationId}/approve` + ) +} + +async function rejectProducerDelegation({ + delegationId, + ...payload +}: { delegationId: string } & RejectDelegationPayload) { + return axiosInstance.post( + `${BACKEND_FOR_FRONTEND_URL}/producer/delegations/${delegationId}/reject`, + payload + ) +} + +async function revokeProducerDelegation({ delegationId }: { delegationId: string }) { + return axiosInstance.delete(`${BACKEND_FOR_FRONTEND_URL}/producer/delegations/${delegationId}`) +} + +async function downloadDelegationContract({ + delegationId, + contractId, +}: { + delegationId: string + contractId: string +}) { + const response = await axiosInstance.get( + `${BACKEND_FOR_FRONTEND_URL}/delegations/${delegationId}/contracts/${contractId}`, + { responseType: 'arraybuffer' } + ) + return response.data +} + +async function createProducerDelegationAndEservice({ + delegateId, + ...crateDraftPayload +}: EServiceSeed & { delegateId: string }) { + const response = await EServiceServices.createDraft(crateDraftPayload) + //!!! Temporary, in order to avoid eventual consistency issues. + await waitFor(4000) + const delegationParams = { + eserviceId: response.id, + delegateId, + } + return await createProducerDelegation(delegationParams) +} + +export const DelegationServices = { + getProducerDelegations, + getSingle, + createProducerDelegation, + approveProducerDelegation, + rejectProducerDelegation, + revokeProducerDelegation, + downloadDelegationContract, + createProducerDelegationAndEservice, +} diff --git a/src/api/delegation/index.ts b/src/api/delegation/index.ts new file mode 100644 index 000000000..126feb943 --- /dev/null +++ b/src/api/delegation/index.ts @@ -0,0 +1,4 @@ +export * from './delegation.mutations' +export * from './delegation.queries' +export * from './delegation.services' +export * from './delegation.downloads' diff --git a/src/api/eservice/eservice.mutations.ts b/src/api/eservice/eservice.mutations.ts index c447a5a0e..da0eda5a2 100644 --- a/src/api/eservice/eservice.mutations.ts +++ b/src/api/eservice/eservice.mutations.ts @@ -104,19 +104,38 @@ function useUpdateVersionDraft(config = { suppressSuccessToast: false }) { }) } -function usePublishVersionDraft() { +function usePublishVersionDraft({ isByDelegation }: { isByDelegation?: boolean }) { const { t } = useTranslation('mutations-feedback', { - keyPrefix: 'eservice.publishVersionDraft', + keyPrefix: isByDelegation + ? 'eservice.publishDelegatedVersionDraft' + : 'eservice.publishVersionDraft', }) return useMutation({ - mutationFn: EServiceServices.publishVersionDraft, + mutationFn: ({ + eserviceId, + descriptorId, + }: { + delegatorName?: string + eserviceName?: string + eserviceId: string + descriptorId: string + }) => EServiceServices.publishVersionDraft({ eserviceId, descriptorId }), meta: { successToastLabel: t('outcome.success'), errorToastLabel: t('outcome.error'), loadingLabel: t('loading'), confirmationDialog: { title: t('confirmDialog.title'), - description: t('confirmDialog.description'), + description: isByDelegation + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + (variables: any) => { + return t('confirmDialog.description', { + delegatorName: variables.delegatorName, + eserviceName: variables.eserviceName, + }) + } + : () => t('confirmDialog.description'), + proceedLabel: isByDelegation ? t('confirmDialog.actions.proceed') : undefined, }, }, }) @@ -332,6 +351,34 @@ function useUpdateDescriptorAttributes() { }) } +function useApproveDelegatedVersionDraft() { + const { t } = useTranslation('mutations-feedback', { + keyPrefix: 'eservice.approveDelegatedVersionDraft', + }) + return useMutation({ + mutationFn: EServiceServices.approveDelegatedVersionDraft, + meta: { + successToastLabel: t('outcome.success'), + errorToastLabel: t('outcome.error'), + loadingLabel: t('loading'), + }, + }) +} + +function useRejectDelegatedVersionDraft() { + const { t } = useTranslation('mutations-feedback', { + keyPrefix: 'eservice.rejectDelegatedVersionDraft', + }) + return useMutation({ + mutationFn: EServiceServices.rejectDelegatedVersionDraft, + meta: { + successToastLabel: t('outcome.success'), + errorToastLabel: t('outcome.error'), + loadingLabel: t('loading'), + }, + }) +} + export const EServiceMutations = { useCreateDraft, useUpdateDraft, @@ -353,4 +400,6 @@ export const EServiceMutations = { useUpdateVersionDraftDocumentDescription, useImportVersion, useUpdateDescriptorAttributes, + useApproveDelegatedVersionDraft, + useRejectDelegatedVersionDraft, } diff --git a/src/api/eservice/eservice.services.ts b/src/api/eservice/eservice.services.ts index 3b921fc87..91a95f45a 100644 --- a/src/api/eservice/eservice.services.ts +++ b/src/api/eservice/eservice.services.ts @@ -22,6 +22,7 @@ import type { ProducerEServiceDescriptor, ProducerEServiceDetails, ProducerEServices, + RejectDelegatedEServiceDescriptorSeed, UpdateEServiceDescriptorDocumentSeed, UpdateEServiceDescriptorQuotas, UpdateEServiceDescriptorSeed, @@ -407,6 +408,34 @@ async function updateDescriptorAttributes({ ) } +async function approveDelegatedVersionDraft({ + eserviceId, + descriptorId, +}: { + eserviceId: string + descriptorId: string +}) { + const response = await axiosInstance.post( + `${BACKEND_FOR_FRONTEND_URL}/eservices/${eserviceId}/descriptors/${descriptorId}/approve` + ) + return response.data +} + +async function rejectDelegatedVersionDraft({ + eserviceId, + descriptorId, + ...payload +}: { + eserviceId: string + descriptorId: string +} & RejectDelegatedEServiceDescriptorSeed) { + const response = await axiosInstance.post( + `${BACKEND_FOR_FRONTEND_URL}/eservices/${eserviceId}/descriptors/${descriptorId}/reject`, + payload + ) + return response.data +} + export const EServiceServices = { getCatalogList, getProviderList, @@ -439,4 +468,6 @@ export const EServiceServices = { exportVersion, importVersion, updateDescriptorAttributes, + approveDelegatedVersionDraft, + rejectDelegatedVersionDraft, } diff --git a/src/api/tenant/tenant.mutations.ts b/src/api/tenant/tenant.mutations.ts index 76cecee99..c6e1f467a 100644 --- a/src/api/tenant/tenant.mutations.ts +++ b/src/api/tenant/tenant.mutations.ts @@ -14,6 +14,36 @@ function useUpdateMail() { }) } +function useAssignTenantDelegatedProducerFeature() { + const { t } = useTranslation('mutations-feedback', { + keyPrefix: 'party.updateProducerDelegationAvailability', + }) + return useMutation({ + mutationFn: TenantServices.assignTenantDelegatedProducerFeature, + meta: { + successToastLabel: t('outcome.success'), + errorToastLabel: t('outcome.error'), + loadingLabel: t('loading'), + }, + }) +} + +function useDeleteTenantDelegatedProducerFeature() { + const { t } = useTranslation('mutations-feedback', { + keyPrefix: 'party.updateProducerDelegationAvailability', + }) + return useMutation({ + mutationFn: TenantServices.deleteTenantDelegatedProducerFeature, + meta: { + successToastLabel: t('outcome.success'), + errorToastLabel: t('outcome.error'), + loadingLabel: t('loading'), + }, + }) +} + export const TenantMutations = { useUpdateMail, + useAssignTenantDelegatedProducerFeature, + useDeleteTenantDelegatedProducerFeature, } diff --git a/src/api/tenant/tenant.services.ts b/src/api/tenant/tenant.services.ts index 52ef741e7..e39635803 100644 --- a/src/api/tenant/tenant.services.ts +++ b/src/api/tenant/tenant.services.ts @@ -1,6 +1,7 @@ import { BACKEND_FOR_FRONTEND_URL } from '@/config/env' import axiosInstance from '@/config/axios' import type { + DelegatedProducer, GetInstitutionUsersParams, GetTenantsParams, MailSeed, @@ -41,9 +42,19 @@ function updateMail({ return axiosInstance.post(`${BACKEND_FOR_FRONTEND_URL}/tenants/${partyId}/mails`, payload) } +function assignTenantDelegatedProducerFeature() { + return axiosInstance.post(`${BACKEND_FOR_FRONTEND_URL}/tenants/delegatedProducer`) +} + +function deleteTenantDelegatedProducerFeature() { + return axiosInstance.delete(`${BACKEND_FOR_FRONTEND_URL}/tenants/delegatedProducer`) +} + export const TenantServices = { getParty, getPartyUsersList, getTenants, updateMail, + assignTenantDelegatedProducerFeature, + deleteTenantDelegatedProducerFeature, } diff --git a/src/components/dialogs/Dialog.tsx b/src/components/dialogs/Dialog.tsx index 92d868519..06e06a7ac 100644 --- a/src/components/dialogs/Dialog.tsx +++ b/src/components/dialogs/Dialog.tsx @@ -17,6 +17,11 @@ import type { DialogSetTenantMailProps, DialogRemoveUserFromKeychainProps, DialogDeleteProducerKeychainKeyProps, + DialogDelegationsProps, + DialogAcceptProducerDelegationProps, + DialogRejectProducerDelegationProps, + DialogRevokeProducerDelegationProps, + DialogRejectDelegatedVersionDraftProps, } from '@/types/dialog.types' import { DialogRejectAgreement } from './DialogRejectAgreement' import { ErrorBoundary } from '../shared/ErrorBoundary' @@ -31,6 +36,11 @@ import { DialogRejectPurposeVersion } from './DialogRejectPurposeVersion' import { DialogSetTenantMail } from './DialogSetTenantMail' import { DialogRemoveUserFromKeychain } from './DialogRemoveUserFromKeychain' import { DialogDeleteProducerKeychainKey } from './DialogDeleteProducerKeychainKey' +import { DialogDelegations } from './DialogDelegations' +import { DialogAcceptProducerDelegation } from './DialogAcceptProducerDelegation' +import { DialogRejectProducerDelegation } from './DialogRejectProducerDelegation' +import { DialogRevokeProducerDelegation } from './DialogRevokeProducerDelegation' +import { DialogRejectDelegatedVersionDraft } from './DialogRejectDelegatedVersionDraft' function match( onBasic: (props: DialogBasicProps) => T, @@ -45,7 +55,12 @@ function match( onRejectPurposeVersion: (props: DialogRejectPurposeVersionProps) => T, onSetTenantMail: (props: DialogSetTenantMailProps) => T, onRemoveUserFromKeychain: (props: DialogRemoveUserFromKeychainProps) => T, - onDeleteProducerKeychainKey: (props: DialogDeleteProducerKeychainKeyProps) => T + onDeleteProducerKeychainKey: (props: DialogDeleteProducerKeychainKeyProps) => T, + onDelegations: (props: DialogDelegationsProps) => T, + onAcceptDelegation: (props: DialogAcceptProducerDelegationProps) => T, + onRejectDelegation: (props: DialogRejectProducerDelegationProps) => T, + onRevokeProducerDelegation: (props: DialogRevokeProducerDelegationProps) => T, + onRejectDelegatedVersionDraft: (props: DialogRejectDelegatedVersionDraftProps) => T ) { return (props: DialogProps) => { switch (props.type) { @@ -75,6 +90,16 @@ function match( return onRemoveUserFromKeychain(props) case 'deleteProducerKeychainKey': return onDeleteProducerKeychainKey(props) + case 'delegations': + return onDelegations(props) + case 'acceptDelegation': + return onAcceptDelegation(props) + case 'rejectDelegation': + return onRejectDelegation(props) + case 'revokeProducerDelegation': + return onRevokeProducerDelegation(props) + case 'rejectDelegatedVersionDraft': + return onRejectDelegatedVersionDraft(props) } } } @@ -92,7 +117,12 @@ const _Dialog = match( (props) => , (props) => , (props) => , - (props) => + (props) => , + (props) => , + (props) => , + (props) => , + (props) => , + (props) => ) export const Dialog: React.FC = () => { diff --git a/src/components/dialogs/DialogAcceptProducerDelegation.tsx b/src/components/dialogs/DialogAcceptProducerDelegation.tsx new file mode 100644 index 000000000..36e2adad2 --- /dev/null +++ b/src/components/dialogs/DialogAcceptProducerDelegation.tsx @@ -0,0 +1,75 @@ +import { DelegationMutations } from '@/api/delegation' +import { useDialog } from '@/stores' +import type { DialogAcceptProducerDelegationProps } from '@/types/dialog.types' +import { + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + Stack, + Typography, +} from '@mui/material' +import React from 'react' +import { useTranslation } from 'react-i18next' + +export const DialogAcceptProducerDelegation: React.FC = ({ + delegationId, +}) => { + const ariaLabelId = React.useId() + + const { t: tCommon } = useTranslation('common', { keyPrefix: 'actions' }) + const { t } = useTranslation('shared-components', { + keyPrefix: 'dialogAcceptProducerDelegation', + }) + + const { closeDialog } = useDialog() + const { mutate: acceptDelegation } = DelegationMutations.useApproveProducerDelegation() + + const [isConfirmCheckboxChecked, setIsConfirmCheckboxChecked] = React.useState(false) + + const handleCheckBoxChange = () => { + setIsConfirmCheckboxChecked((prev) => { + return !prev + }) + } + + const handleAccept = () => { + acceptDelegation({ delegationId }) + closeDialog() + } + + return ( + + {t('title')} + + + + + {t('content.description')} + + + } + label={t('content.confirmationLabel')} + sx={{ mx: 1 }} + /> + + + + + + + + + ) +} diff --git a/src/components/dialogs/DialogDelegations.tsx b/src/components/dialogs/DialogDelegations.tsx new file mode 100644 index 000000000..574c6dd1a --- /dev/null +++ b/src/components/dialogs/DialogDelegations.tsx @@ -0,0 +1,70 @@ +import { useDialog } from '@/stores' +import { + Box, + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + Typography, +} from '@mui/material' +import React from 'react' +import { useTranslation } from 'react-i18next' + +export type DialogDelegationsProps = { + onConfirm: () => void +} + +export const DialogDelegations: React.FC = ({ onConfirm }) => { + const ariaLabelId = React.useId() + + const { t } = useTranslation('party', { + keyPrefix: 'delegations.create.dialog', + }) + + const { closeDialog } = useDialog() + + const [isCheckboxChecked, setIsCheckboxChecked] = React.useState(false) + + const handleCheckBoxChange = () => { + setIsCheckboxChecked((prev) => { + return !prev + }) + } + + const onSubmit = () => { + onConfirm() + closeDialog() + } + + return ( + + + {t('title')} + + + {t('description')} + + } + label={t('checkboxLabel')} + sx={{ mx: 1 }} + /> + + + + + + + + ) +} diff --git a/src/components/dialogs/DialogRejectAgreement.tsx b/src/components/dialogs/DialogRejectAgreement.tsx index f06513950..c41b908a2 100644 --- a/src/components/dialogs/DialogRejectAgreement.tsx +++ b/src/components/dialogs/DialogRejectAgreement.tsx @@ -25,7 +25,8 @@ export const DialogRejectAgreement: React.FC = ({ ag }) const onSubmit = ({ reason }: RejectAgreementFormValues) => { - reject({ agreementId, reason }, { onSuccess: closeDialog }) + reject({ agreementId, reason }) + closeDialog() } return ( diff --git a/src/components/dialogs/DialogRejectDelegatedVersionDraft.tsx b/src/components/dialogs/DialogRejectDelegatedVersionDraft.tsx new file mode 100644 index 000000000..811961bf6 --- /dev/null +++ b/src/components/dialogs/DialogRejectDelegatedVersionDraft.tsx @@ -0,0 +1,78 @@ +import { EServiceMutations } from '@/api/eservice' +import { useDialog } from '@/stores' +import type { DialogRejectDelegatedVersionDraftProps } from '@/types/dialog.types' +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Typography, +} from '@mui/material' +import React from 'react' +import { FormProvider, useForm } from 'react-hook-form' +import { useTranslation } from 'react-i18next' +import { RHFTextField } from '../shared/react-hook-form-inputs' + +type RejectDelegatedVersionDraftFormValues = { + reason: string +} + +export const DialogRejectDelegatedVersionDraft: React.FC< + DialogRejectDelegatedVersionDraftProps +> = ({ eserviceId, descriptorId }) => { + const ariaLabelId = React.useId() + + const { t } = useTranslation('shared-components', { + keyPrefix: 'dialogRejectDelegatedVersionDraft', + }) + const { t: tCommon } = useTranslation('common', { keyPrefix: 'actions' }) + const { closeDialog } = useDialog() + const { mutate: rejectDelegatedVersionDraft } = EServiceMutations.useRejectDelegatedVersionDraft() + + const formMethods = useForm({ + defaultValues: { reason: '' }, + }) + + const onSubmit = ({ reason }: RejectDelegatedVersionDraftFormValues) => { + rejectDelegatedVersionDraft({ eserviceId, descriptorId, rejectionReason: reason }) + closeDialog() + } + + return ( + + + + + {t('title')} + + + + + {t('description')} + + + + + + + + + + + + ) +} diff --git a/src/components/dialogs/DialogRejectProducerDelegation.tsx b/src/components/dialogs/DialogRejectProducerDelegation.tsx new file mode 100644 index 000000000..33d610689 --- /dev/null +++ b/src/components/dialogs/DialogRejectProducerDelegation.tsx @@ -0,0 +1,68 @@ +import { DelegationMutations } from '@/api/delegation' +import { useDialog } from '@/stores' +import type { DialogRejectProducerDelegationProps } from '@/types/dialog.types' +import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material' +import React from 'react' +import { FormProvider, useForm } from 'react-hook-form' +import { RHFTextField } from '../shared/react-hook-form-inputs' +import { useTranslation } from 'react-i18next' + +type RejectDelegationFormValues = { + reason: string +} + +export const DialogRejectProducerDelegation: React.FC = ({ + delegationId, +}) => { + const ariaLabelId = React.useId() + + const { t: tCommon } = useTranslation('common', { keyPrefix: 'actions' }) + const { t } = useTranslation('shared-components', { + keyPrefix: 'dialogRejectProducerDelegation', + }) + const { closeDialog } = useDialog() + + const { mutate: rejectDelegation } = DelegationMutations.useRejectProducerDelegation() + + const formMethods = useForm({ + defaultValues: { reason: '' }, + }) + + const onSubmit = ({ reason }: RejectDelegationFormValues) => { + rejectDelegation({ delegationId, rejectionReason: reason }) + closeDialog() + } + + return ( + + + + + {t('title')} + + + + + + + + + + + + + + ) +} diff --git a/src/components/dialogs/DialogRejectPurposeVersion.tsx b/src/components/dialogs/DialogRejectPurposeVersion.tsx index b743c0ea5..ed550eb62 100644 --- a/src/components/dialogs/DialogRejectPurposeVersion.tsx +++ b/src/components/dialogs/DialogRejectPurposeVersion.tsx @@ -37,7 +37,8 @@ export const DialogRejectPurposeVersion: React.FC { - rejectVersion({ purposeId, versionId, rejectionReason: reason }, { onSuccess: closeDialog }) + rejectVersion({ purposeId, versionId, rejectionReason: reason }) + closeDialog() } return ( diff --git a/src/components/dialogs/DialogRevokeCertifiedAttribute.tsx b/src/components/dialogs/DialogRevokeCertifiedAttribute.tsx index 071059dbe..a3cf34720 100644 --- a/src/components/dialogs/DialogRevokeCertifiedAttribute.tsx +++ b/src/components/dialogs/DialogRevokeCertifiedAttribute.tsx @@ -39,10 +39,8 @@ export const DialogRevokeCertifiedAttribute: React.FC { - revokeCertifiedAttribute( - { tenantId: attribute.tenantId, attributeId: attribute.attributeId }, - { onSuccess: closeDialog } - ) + revokeCertifiedAttribute({ tenantId: attribute.tenantId, attributeId: attribute.attributeId }) + closeDialog() } return ( diff --git a/src/components/dialogs/DialogRevokeProducerDelegation.tsx b/src/components/dialogs/DialogRevokeProducerDelegation.tsx new file mode 100644 index 000000000..f5a132529 --- /dev/null +++ b/src/components/dialogs/DialogRevokeProducerDelegation.tsx @@ -0,0 +1,82 @@ +import { DelegationMutations } from '@/api/delegation' +import { useDialog } from '@/stores' +import type { DialogRevokeProducerDelegationProps } from '@/types/dialog.types' +import { + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + Link, + Stack, + Typography, +} from '@mui/material' +import React from 'react' +import { Trans, useTranslation } from 'react-i18next' + +export const DialogRevokeProducerDelegation: React.FC = ({ + delegationId, + eserviceName, +}) => { + const ariaLabelId = React.useId() + const { closeDialog } = useDialog() + const { t: tCommon } = useTranslation('common', { keyPrefix: 'actions' }) + const { t } = useTranslation('shared-components', { keyPrefix: 'dialogRevokeProducerDelegation' }) + + const [isConfirmCheckboxChecked, setIsConfirmCheckboxChecked] = React.useState(false) + + const { mutate: revokeDelegation } = DelegationMutations.useRevokeProducerDelegation() + + const handleCheckBoxChange = () => { + setIsConfirmCheckboxChecked((prev) => { + return !prev + }) + } + + const handleRevoke = () => { + revokeDelegation({ delegationId }) + closeDialog() + } + + return ( + + {t('title')} + + + + + , + strong: , + }} + > + {t('content.description', { + eserviceName: eserviceName, + })} + + + } + label={t('content.checkbox')} + sx={{ mx: 1 }} + /> + + + + + + + + + ) +} diff --git a/src/components/dialogs/DialogSetTenantMail.tsx b/src/components/dialogs/DialogSetTenantMail.tsx index 364b6ce93..370d8fd33 100644 --- a/src/components/dialogs/DialogSetTenantMail.tsx +++ b/src/components/dialogs/DialogSetTenantMail.tsx @@ -48,18 +48,14 @@ export const DialogSetTenantMail: React.FC = () => { if (!jwt?.organizationId) return if (!isEqual(defaultValues, values)) { const { contactEmail, description } = values - setMail( - { - partyId: jwt.organizationId, - address: contactEmail, - kind: 'CONTACT_EMAIL', - description: description || undefined, - }, - { - onSuccess: closeDialog, - } - ) + setMail({ + partyId: jwt.organizationId, + address: contactEmail, + kind: 'CONTACT_EMAIL', + description: description || undefined, + }) } + closeDialog() } return ( diff --git a/src/components/layout/SideNav/hooks/__tests__/useGetSideNavItems.test.ts b/src/components/layout/SideNav/hooks/__tests__/useGetSideNavItems.test.ts index a34874fdb..45839a4b2 100644 --- a/src/components/layout/SideNav/hooks/__tests__/useGetSideNavItems.test.ts +++ b/src/components/layout/SideNav/hooks/__tests__/useGetSideNavItems.test.ts @@ -43,6 +43,7 @@ describe('useGetSideNavItems', () => { { "children": [ "PARTY_REGISTRY", + "DELEGATIONS", ], "id": "tenant", "routeKey": "TENANT", @@ -188,6 +189,7 @@ describe('useGetSideNavItems', () => { { "children": [ "PARTY_REGISTRY", + "DELEGATIONS", ], "id": "tenant", "routeKey": "TENANT", @@ -228,6 +230,7 @@ describe('useGetSideNavItems', () => { { "children": [ "PARTY_REGISTRY", + "DELEGATIONS", ], "id": "tenant", "routeKey": "TENANT", @@ -268,6 +271,7 @@ describe('useGetSideNavItems', () => { { "children": [ "PARTY_REGISTRY", + "DELEGATIONS", ], "id": "tenant", "routeKey": "TENANT", diff --git a/src/components/layout/SideNav/hooks/useGetSideNavItems.ts b/src/components/layout/SideNav/hooks/useGetSideNavItems.ts index 065d239df..27f48c5f1 100644 --- a/src/components/layout/SideNav/hooks/useGetSideNavItems.ts +++ b/src/components/layout/SideNav/hooks/useGetSideNavItems.ts @@ -4,6 +4,7 @@ import type { RouteKey } from '@/router' import { routes } from '@/router' import { AuthHooks } from '@/api/auth' import { TenantHooks } from '@/api/tenant' +import { isTenantCertifier } from '@/utils/tenant.utils' const views = [ { @@ -28,7 +29,11 @@ const views = [ 'PROVIDE_KEYCHAINS_LIST', ], }, - { routeKey: 'TENANT', id: 'tenant', children: ['PARTY_REGISTRY', 'TENANT_CERTIFIER'] }, + { + routeKey: 'TENANT', + id: 'tenant', + children: ['PARTY_REGISTRY', 'TENANT_CERTIFIER', 'DELEGATIONS'], + }, ] as const export function useGetSideNavItems() { @@ -36,7 +41,7 @@ export function useGetSideNavItems() { const { data: tenant } = TenantHooks.useGetActiveUserParty() - const isCertifier = Boolean(tenant.features[0]?.certifier?.certifierId) + const isCertifier = isTenantCertifier(tenant) return React.useMemo(() => { /** diff --git a/src/components/shared/ByDelegationChip.tsx b/src/components/shared/ByDelegationChip.tsx new file mode 100644 index 000000000..0dd742471 --- /dev/null +++ b/src/components/shared/ByDelegationChip.tsx @@ -0,0 +1,13 @@ +import { Chip, Skeleton } from '@mui/material' +import React from 'react' +import { useTranslation } from 'react-i18next' + +export const ByDelegationChip: React.FC = () => { + const { t } = useTranslation('shared-components', { keyPrefix: 'byDelegationChip' }) + + return +} + +export const ByDelegationChipSkeleton: React.FC = () => { + return +} diff --git a/src/components/shared/DelegationTable/DelegationsTable.tsx b/src/components/shared/DelegationTable/DelegationsTable.tsx new file mode 100644 index 000000000..341cf3186 --- /dev/null +++ b/src/components/shared/DelegationTable/DelegationsTable.tsx @@ -0,0 +1,79 @@ +import { DelegationQueries } from '@/api/delegation' +import { Table } from '@pagopa/interop-fe-commons' +import { useSuspenseQuery } from '@tanstack/react-query' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { DelegationsTableRow, DelegationsTableRowSkeleton } from './DelegationsTableRow' +import { match } from 'ts-pattern' +import type { DelegationType } from '@/types/party.types' +import type { GetDelegationsParams } from '@/api/api.generatedTypes' + +type DelegationsTableProps = { + params: GetDelegationsParams + delegationType: DelegationType +} + +export const DelegationsTable: React.FC = ({ params, delegationType }) => { + const { t } = useTranslation('party', { keyPrefix: 'delegations' }) + const { t: tCommon } = useTranslation('common', { keyPrefix: 'table.headData' }) + const { data: delegations } = useSuspenseQuery({ + ...DelegationQueries.getProducerDelegationsList(params), + select: ({ results }) => results, + }) + + const delegateOrDelegatorHeadLabel = match(delegationType) + .with('DELEGATION_RECEIVED', () => tCommon('delegatorName')) + .with('DELEGATION_GRANTED', () => tCommon('delegateName')) + .exhaustive() + + const headLabels = [ + tCommon('eserviceName'), + tCommon('delegationKind'), + delegateOrDelegatorHeadLabel, + tCommon('status'), + '', + ] + + const isEmpty = !delegations || delegations.length === 0 + + return ( + + {delegations.map((delegation) => ( + + ))} +
+ ) +} + +export const DelegationsTableSkeleton: React.FC> = ({ + delegationType, +}) => { + const { t: tCommon } = useTranslation('common', { keyPrefix: 'table.headData' }) + + const delegateOrDelegatorHeadLabel = match(delegationType) + .with('DELEGATION_RECEIVED', () => tCommon('delegatorName')) + .with('DELEGATION_GRANTED', () => tCommon('delegateName')) + .exhaustive() + + const headLabels = [ + tCommon('eserviceName'), + tCommon('delegationKind'), + delegateOrDelegatorHeadLabel, + tCommon('status'), + '', + ] + + return ( + + + + + + +
+ ) +} diff --git a/src/components/shared/DelegationTable/DelegationsTableRow.tsx b/src/components/shared/DelegationTable/DelegationsTableRow.tsx new file mode 100644 index 000000000..f4b6a7536 --- /dev/null +++ b/src/components/shared/DelegationTable/DelegationsTableRow.tsx @@ -0,0 +1,87 @@ +import type { CompactDelegation } from '@/api/api.generatedTypes' +import { DelegationQueries } from '@/api/delegation' +import { ActionMenu, ActionMenuSkeleton } from '@/components/shared/ActionMenu' +import { ButtonSkeleton } from '@/components/shared/MUI-skeletons' +import { StatusChip, StatusChipSkeleton } from '@/components/shared/StatusChip' +import { Link } from '@/router' +import { Box, Skeleton } from '@mui/material' +import { TableRow } from '@pagopa/interop-fe-commons' +import { useQueryClient } from '@tanstack/react-query' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { match } from 'ts-pattern' +import { useGetDelegationActions } from '@/hooks/useGetDelegationActions' +import type { DelegationType } from '@/types/party.types' + +type DelegationsTableRowProps = { + delegation: CompactDelegation + delegationType: DelegationType +} + +export const DelegationsTableRow: React.FC = ({ + delegation, + delegationType, +}) => { + const { t: tCommon } = useTranslation('common') + const { t } = useTranslation('party', { keyPrefix: 'delegations.list' }) + const queryClient = useQueryClient() + + const { actions } = useGetDelegationActions(delegation) + + const handlePrefetch = () => { + queryClient.prefetchQuery(DelegationQueries.getSingle({ delegationId: delegation.id })) + } + + const delegationKindLabel = match(delegation.kind) + .with('DELEGATED_PRODUCER', () => t('delegationKind.producer')) + .with('DELEGATED_CONSUMER', () => t('delegationKind.consumer')) + .exhaustive() + + const delegateOrDelegatorCellData = match(delegationType) + .with('DELEGATION_RECEIVED', () => delegation.delegator.name) + .with('DELEGATION_GRANTED', () => delegation.delegate.name) + .exhaustive() + + return ( + , + ]} + > + + {tCommon('actions.inspect')} + + + + + + + ) +} + +export const DelegationsTableRowSkeleton: React.FC = () => { + return ( + , + , + , + , + ]} + > + + + + ) +} diff --git a/src/components/shared/DelegationTable/index.ts b/src/components/shared/DelegationTable/index.ts new file mode 100644 index 000000000..c0ada9fe9 --- /dev/null +++ b/src/components/shared/DelegationTable/index.ts @@ -0,0 +1 @@ +export * from './DelegationsTable' diff --git a/src/components/shared/StatusChip.tsx b/src/components/shared/StatusChip.tsx index 7c887d42f..8756c324e 100644 --- a/src/components/shared/StatusChip.tsx +++ b/src/components/shared/StatusChip.tsx @@ -9,6 +9,7 @@ import type { Agreement, AgreementListEntry, AgreementState, + DelegationState, EServiceDescriptorState, Purpose, PurposeVersionState, @@ -20,6 +21,7 @@ const CHIP_COLORS_E_SERVICE: Record = { SUSPENDED: 'error', ARCHIVED: 'info', DEPRECATED: 'warning', + WAITING_FOR_APPROVAL: 'warning', } const CHIP_COLORS_AGREEMENT: Record = { @@ -41,10 +43,18 @@ const CHIP_COLORS_PURPOSE: Record = { REJECTED: 'error', } +const CHIP_COLORS_DELEGATION: Record = { + ACTIVE: 'success', + REJECTED: 'error', + REVOKED: 'error', + WAITING_FOR_APPROVAL: 'warning', +} + const chipColors = { eservice: CHIP_COLORS_E_SERVICE, agreement: CHIP_COLORS_AGREEMENT, purpose: CHIP_COLORS_PURPOSE, + delegation: CHIP_COLORS_DELEGATION, } as const type StatusChipProps = Omit & @@ -52,6 +62,7 @@ type StatusChipProps = Omit & | { for: 'eservice' state: EServiceDescriptorState + isDraftToCorrect?: boolean } | { for: 'agreement' @@ -61,6 +72,10 @@ type StatusChipProps = Omit & for: 'purpose' purpose: Purpose } + | { + for: 'delegation' + state: DelegationState + } ) function getAgreementChipState( @@ -102,8 +117,10 @@ export const StatusChip: React.FC = (props) => { let label = '' if (props.for === 'eservice') { - color = chipColors['eservice'][props.state] - label = t(`status.eservice.${props.state}`) + color = props.isDraftToCorrect ? 'warning' : chipColors['eservice'][props.state] + label = props.isDraftToCorrect + ? t('status.eservice.DRAFT_TO_CORRECT') + : t(`status.eservice.${props.state}`) } if (props.for === 'agreement') { @@ -120,6 +137,11 @@ export const StatusChip: React.FC = (props) => { return } + if (props.for === 'delegation') { + color = chipColors['delegation'][props.state] + label = t(`status.delegation.${props.state}`) + } + return ( ({ + ...(await importOriginal()), + useQuery: vi.fn(), + useQueries: vi.fn(), +})) + +import { useQuery } from '@tanstack/react-query' +import { mockUseJwt } from '@/utils/testing.utils' +import { renderHook } from '@testing-library/react' +import { useGetDelegationUserRole } from '../useGetDelegationUserRole' + +const mockUseGetProducerDelegationsList = (data: Array | undefined) => + (useQuery as Mock).mockReturnValue({ + data, + } as never) + +describe('useGetDelegationUserRole tests', () => { + it('should return the isDelegator true and isDelegate false if there is a delegation with the organization as delegator', () => { + mockUseGetProducerDelegationsList([ + { + id: '1', + delegator: { id: 'organizationId', name: 'delegator' }, + delegate: { id: 'delegateId', name: 'delegate' }, + state: 'ACTIVE', + kind: 'DELEGATED_PRODUCER', + eservice: { id: 'eserviceId', name: 'eservice' }, + }, + ]) + const { result } = renderHook(() => + useGetDelegationUserRole({ eserviceId: 'eserviceId', organizationId: 'organizationId' }) + ) + expect(result.current.isDelegator).toBe(true) + expect(result.current.isDelegate).toBe(false) + expect(result.current.producerDelegations?.length).toBe(1) + }) + + it('should return the isDelegator false and isDelegate true if there is a delegation with the organization as delegate', () => { + mockUseGetProducerDelegationsList([ + { + id: '1', + delegator: { id: 'delegatorId', name: 'delegator' }, + delegate: { id: 'organizationId', name: 'delegate' }, + state: 'ACTIVE', + kind: 'DELEGATED_PRODUCER', + eservice: { id: 'eserviceId', name: 'eservice' }, + }, + ]) + const { result } = renderHook(() => + useGetDelegationUserRole({ eserviceId: 'eserviceId', organizationId: 'organizationId' }) + ) + expect(result.current.isDelegator).toBe(false) + expect(result.current.isDelegate).toBe(true) + expect(result.current.producerDelegations?.length).toBe(1) + }) + + it('should return the isDelegator false and isDelegate false if there are no delegations for the eservice', () => { + mockUseGetProducerDelegationsList([]) + const { result } = renderHook(() => + useGetDelegationUserRole({ eserviceId: 'eserviceId', organizationId: 'organizationId' }) + ) + expect(result.current.isDelegator).toBe(false) + expect(result.current.isDelegate).toBe(false) + expect(result.current.producerDelegations?.length).toBe(0) + }) +}) diff --git a/src/hooks/__tests__/useGetProviderEServiceActions.test.ts b/src/hooks/__tests__/useGetProviderEServiceActions.test.ts index 4dca3da43..ac5a004f4 100644 --- a/src/hooks/__tests__/useGetProviderEServiceActions.test.ts +++ b/src/hooks/__tests__/useGetProviderEServiceActions.test.ts @@ -6,10 +6,27 @@ import { setupServer } from 'msw/node' import { BACKEND_FOR_FRONTEND_URL } from '@/config/env' import { act } from 'react-dom/test-utils' import { fireEvent, screen, waitFor } from '@testing-library/react' -import type { ProducerEService } from '@/api/api.generatedTypes' +import type { CompactDelegation, ProducerEService } from '@/api/api.generatedTypes' +import * as hooks from '@/hooks/useGetDelegationUserRole' mockUseJwt({ isAdmin: true }) +const mockUseGetDelegationUserRole = ({ + isDelegator = false, + isDelegate = false, + producerDelegations = [], +}: { + isDelegator?: boolean + isDelegate?: boolean + producerDelegations?: CompactDelegation[] +}) => { + vi.spyOn(hooks, 'useGetDelegationUserRole').mockReturnValue({ + isDelegator, + isDelegate, + producerDelegations, + }) +} + const server = setupServer( rest.post( `${BACKEND_FOR_FRONTEND_URL}/eservices/ad474d35-7939-4bee-bde9-4e469cca1030/descriptors/test-1/clone`, @@ -44,6 +61,7 @@ function renderUseGetProviderEServiceTableActionsHook(descriptorMock: ProducerES useGetProviderEServiceActions( descriptorMock.id, descriptorMock.activeDescriptor?.state, + descriptorMock.draftDescriptor?.state, descriptorMock.activeDescriptor?.id, descriptorMock.draftDescriptor?.id, descriptorMock.mode @@ -56,14 +74,123 @@ function renderUseGetProviderEServiceTableActionsHook(descriptorMock: ProducerES } describe('useGetProviderEServiceTableActions tests', () => { - it('should return the correct actions if the e-service has no descriptors', () => { - const descriptorMock = createMockEServiceProvider() + it('should return the correct actions if the user is admin and e-service is DRAFT with no active descriptors', () => { + mockUseGetDelegationUserRole({}) + const descriptorMock = createMockEServiceProvider({ + draftDescriptor: { id: 'test-1', state: 'DRAFT', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(2) + expect(result.current.actions[0].label).toBe('publishDraft') + expect(result.current.actions[1].label).toBe('delete') + }) + + it('should not return actions if user is admin and delegator, e-service is DRAFT with no active descriptors', () => { + mockUseGetDelegationUserRole({ isDelegator: true }) + const descriptorMock = createMockEServiceProvider({ + draftDescriptor: { id: 'test-1', state: 'DRAFT', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + + it('should return the correct actions if user is admin and delegate, e-service is DRAFT with no active descriptors', () => { + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + draftDescriptor: { id: 'test-1', state: 'DRAFT', version: '1' }, + }) const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) expect(result.current.actions).toHaveLength(1) - expect(result.current.actions[0].label).toBe('delete') + expect(result.current.actions[0].label).toBe('publishDraft') }) - it('should return the correct actions if the e-service has an active descriptor in PUBLISHED state and has no version draft', () => { + it('should not return actions if user is admin and e-service is WAITING_FOR_APPROVAL with no active descriptors', () => { + mockUseGetDelegationUserRole({}) + const descriptorMock = createMockEServiceProvider({ + draftDescriptor: { id: 'test-1', state: 'WAITING_FOR_APPROVAL', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + + it('should return the correct actions if user is admin and delegator, e-service is WAITING_FOR_APPROVAL with no active descriptors', () => { + mockUseGetDelegationUserRole({ isDelegator: true }) + const descriptorMock = createMockEServiceProvider({ + draftDescriptor: { id: 'test-1', state: 'WAITING_FOR_APPROVAL', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(2) + expect(result.current.actions[0].label).toBe('approve') + expect(result.current.actions[1].label).toBe('reject') + }) + + it('should not return actions if user is admin and delegate, e-service is WAITING_FOR_APPROVAL with no active descriptors', () => { + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + draftDescriptor: { id: 'test-1', state: 'WAITING_FOR_APPROVAL', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + + it('should not return actions if user is admin and e-service is ARCHIVED', () => { + mockUseGetDelegationUserRole({}) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'ARCHIVED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + + it('should not return actions if user is admin and delegator, e-service is ARCHIVED', () => { + mockUseGetDelegationUserRole({ isDelegator: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'ARCHIVED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + + it('should not return actions if user is admin and delegate, e-service is ARCHIVED', () => { + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'ARCHIVED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + + it('should return the correct actions if user is admin and e-service is DEPRECATED', () => { + mockUseGetDelegationUserRole({}) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'DEPRECATED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(1) + expect(result.current.actions[0].label).toBe('suspend') + }) + + it('should not return actions if user is admin and delegator, e-service is DEPRECATED', () => { + mockUseGetDelegationUserRole({ isDelegator: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'DEPRECATED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + + it('should return the correct actions if user is admin and delegate, e-service is DEPRECATED', () => { + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'DEPRECATED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(1) + expect(result.current.actions[0].label).toBe('suspend') + }) + + it('should return the correct actions if user is admin and e-service is PUBLISHED with no draft descriptors', () => { + mockUseGetDelegationUserRole({}) const descriptorMock = createMockEServiceProvider({ activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, }) @@ -74,7 +201,8 @@ describe('useGetProviderEServiceTableActions tests', () => { expect(result.current.actions[2].label).toBe('suspend') }) - it('should return the correct actions if the e-service has an active descriptor in PUBLISHED state and has a version draft', () => { + it('should return the correct actions if user is admin and e-service is PUBLISHED with a draft descriptor in state DRAFT', () => { + mockUseGetDelegationUserRole({}) const descriptorMock = createMockEServiceProvider({ activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, draftDescriptor: { id: 'test-2', state: 'DRAFT', version: '2' }, @@ -87,34 +215,74 @@ describe('useGetProviderEServiceTableActions tests', () => { expect(result.current.actions[3].label).toBe('suspend') }) - it('should return no actions if the e-service has an active descriptor in ARCHIVED state', () => { + it('should not return actions if user is admin and delegator, e-service is PUBLISHED with no draft descriptors', () => { + mockUseGetDelegationUserRole({ isDelegator: true }) const descriptorMock = createMockEServiceProvider({ - activeDescriptor: { id: 'test-1', state: 'ARCHIVED', version: '1' }, + activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, }) const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) expect(result.current.actions).toHaveLength(0) }) - it('should return the correct actions if the e-service has an active descriptor in DEPRECATED state', () => { + it('should not return actions if user is admin and delegator, e-service is PUBLISHED with a draft descriptor in state DRAFT', () => { + mockUseGetDelegationUserRole({ isDelegator: true }) const descriptorMock = createMockEServiceProvider({ - activeDescriptor: { id: 'test-1', state: 'DEPRECATED', version: '1' }, + activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, + draftDescriptor: { id: 'test-2', state: 'DRAFT', version: '2' }, }) const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) - expect(result.current.actions).toHaveLength(1) - expect(result.current.actions[0].label).toBe('suspend') + expect(result.current.actions).toHaveLength(0) }) - it('should return the correct actions if the e-service has no active descriptor', () => { + it('should return the correct actions if user is admin and delegator, e-service is PUBLISHED with a draft descriptor in state WAITING_FOR_APPROVAL', () => { + mockUseGetDelegationUserRole({ isDelegator: true }) const descriptorMock = createMockEServiceProvider({ - draftDescriptor: { id: 'test-1', state: 'DRAFT', version: '1' }, + activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, + draftDescriptor: { id: 'test-2', state: 'WAITING_FOR_APPROVAL', version: '2' }, }) const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) expect(result.current.actions).toHaveLength(2) - expect(result.current.actions[0].label).toBe('publishDraft') - expect(result.current.actions[1].label).toBe('delete') + expect(result.current.actions[0].label).toBe('approve') + expect(result.current.actions[1].label).toBe('reject') }) - it('should return the correct actions if the e-service has an active descriptor in SUSPENDED state and has no version draft', () => { + it('should return the correct actions if user is admin and delegate, e-service is PUBLISHED with no draft descriptors', () => { + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(2) + expect(result.current.actions[0].label).toBe('createNewDraft') + expect(result.current.actions[1].label).toBe('suspend') + }) + + it('should return the correct actions if user is admin and delegate, e-service is PUBLISHED with a draft descriptor in state DRAFT', () => { + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, + draftDescriptor: { id: 'test-2', state: 'DRAFT', version: '2' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(3) + expect(result.current.actions[0].label).toBe('manageDraft') + expect(result.current.actions[1].label).toBe('deleteDraft') + expect(result.current.actions[2].label).toBe('suspend') + }) + + it('should return the correct actions if user is admin and delegate, e-service is PUBLISHED with a draft descriptor in state WAITING_FOR_APPROVAL', () => { + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, + draftDescriptor: { id: 'test-2', state: 'WAITING_FOR_APPROVAL', version: '2' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(1) + expect(result.current.actions[0].label).toBe('suspend') + }) + + it('should return the correct actions if user is admin and e-service is SUSPENDED with no draft descriptors', () => { + mockUseGetDelegationUserRole({}) const descriptorMock = createMockEServiceProvider({ activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, }) @@ -125,7 +293,8 @@ describe('useGetProviderEServiceTableActions tests', () => { expect(result.current.actions[2].label).toBe('createNewDraft') }) - it('should return the correct actions if the e-service has an active descriptor in SUSPENDED state and has a version draft', () => { + it('should return the correct actions if user is admin and e-service is SUSPENDED with a draft descriptor in state DRAFT', () => { + mockUseGetDelegationUserRole({}) const descriptorMock = createMockEServiceProvider({ activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, draftDescriptor: { id: 'test-2', state: 'DRAFT', version: '2' }, @@ -138,83 +307,212 @@ describe('useGetProviderEServiceTableActions tests', () => { expect(result.current.actions[3].label).toBe('deleteDraft') }) - it('should navigate to PROVIDE_ESERVICE_EDIT page on clone action success', async () => { + it('should not return actions if user is admin and delegator, e-service is SUSPENDED with no draft descriptors', () => { + mockUseGetDelegationUserRole({ isDelegator: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + + it('should not return actions if user is admin and delegator, e-service is SUSPENDED with a draft descriptor in state DRAFT', () => { + mockUseGetDelegationUserRole({ isDelegator: true }) const descriptorMock = createMockEServiceProvider({ activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, draftDescriptor: { id: 'test-2', state: 'DRAFT', version: '2' }, }) - const { result, history } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) - const cloneAction = result.current.actions[1] + it('should return the correct actions if user is admin and delegator, e-service is SUSPENDED with a draft descriptor in state WAITING_FOR_APPROVAL', () => { + mockUseGetDelegationUserRole({ isDelegator: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, + draftDescriptor: { id: 'test-2', state: 'WAITING_FOR_APPROVAL', version: '2' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(2) + expect(result.current.actions[0].label).toBe('approve') + expect(result.current.actions[1].label).toBe('reject') + }) - expect(cloneAction.label).toBe('clone') + it('should return the correct actions if user is admin and delegate, e-service is SUSPENDED with no draft descriptors', () => { + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(2) + expect(result.current.actions[0].label).toBe('activate') + expect(result.current.actions[1].label).toBe('createNewDraft') + }) - act(() => { - cloneAction.action() + it('should return the correct actions if user is admin and delegate, e-service is SUSPENDED with a draft descriptor in state DRAFT', () => { + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, + draftDescriptor: { id: 'test-2', state: 'DRAFT', version: '2' }, }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(3) + expect(result.current.actions[0].label).toBe('activate') + expect(result.current.actions[1].label).toBe('manageDraft') + expect(result.current.actions[2].label).toBe('deleteDraft') + }) - act(() => { - fireEvent.click(screen.getByRole('button', { name: 'confirm' })) + it('should return the correct actions if user is admin and delegate, e-service is SUSPENDED with a draft descriptor in state WAITING_FOR_APPROVAL', () => { + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, + draftDescriptor: { id: 'test-2', state: 'WAITING_FOR_APPROVAL', version: '2' }, }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(1) + expect(result.current.actions[0].label).toBe('activate') + }) - await waitFor(() => { - expect(history.location.pathname).toBe( - '/it/erogazione/e-service/6dbb7416-8315-4970-a6be-393a03d0a79d/fd09a069-81f8-4cb5-a302-64320e83a033/modifica' - ) + it('should return the correct actions if user is an api operator and e-service is DRAFT with no active descriptors', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({}) + const descriptorMock = createMockEServiceProvider({ + draftDescriptor: { id: 'test-1', state: 'DRAFT', version: '1' }, }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(2) + expect(result.current.actions[0].label).toBe('publishDraft') + expect(result.current.actions[1].label).toBe('delete') }) - it('should navigate to PROVIDE_ESERVICE_EDIT page on create new draft action success', async () => { + it('should not return actions if user is an api operator and delegator, e-service is DRAFT with no active descriptors', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegator: true }) const descriptorMock = createMockEServiceProvider({ - activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, + draftDescriptor: { id: 'test-1', state: 'DRAFT', version: '1' }, }) - const { result, history } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) - const cloneAction = result.current.actions[2] + it('should return the correct actions if user is an api operator and delegate, e-service is DRAFT with no active descriptors', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + draftDescriptor: { id: 'test-1', state: 'DRAFT', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(1) + expect(result.current.actions[0].label).toBe('publishDraft') + }) - expect(cloneAction.label).toBe('createNewDraft') + it('should not return actions if user is an api operator and e-service is WAITING_FOR_APPROVAL with no active descriptors', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({}) + const descriptorMock = createMockEServiceProvider({ + draftDescriptor: { id: 'test-1', state: 'WAITING_FOR_APPROVAL', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) - act(() => { - cloneAction.action() + it('should return the correct actions if user is an api operator and delegator, e-service is WAITING_FOR_APPROVAL with no active descriptors', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegator: true }) + const descriptorMock = createMockEServiceProvider({ + draftDescriptor: { id: 'test-1', state: 'WAITING_FOR_APPROVAL', version: '1' }, }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(2) + expect(result.current.actions[0].label).toBe('approve') + expect(result.current.actions[1].label).toBe('reject') + }) - act(() => { - fireEvent.click(screen.getByRole('button', { name: 'confirm' })) + it('should not return actions if user is an api operator and delegate, e-service is WAITING_FOR_APPROVAL with no active descriptors', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + draftDescriptor: { id: 'test-1', state: 'WAITING_FOR_APPROVAL', version: '1' }, }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) - await waitFor(() => { - expect(history.location.pathname).toBe( - '/it/erogazione/e-service/ad474d35-7939-4bee-bde9-4e469cca1030/test-id/modifica' - ) + it('should not return actions if user is an api operator and e-service is ARCHIVED', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({}) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'ARCHIVED', version: '1' }, }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) }) - it('should not return actions if the user is a security operator', () => { - mockUseJwt({ isAdmin: false, isOperatorSecurity: true }) + it('should not return actions if user is an api operator and delegator, e-service is ARCHIVED', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegator: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'ARCHIVED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + it('should not return actions if user is an api operator and delegate, e-service is ARCHIVED', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegate: true }) const descriptorMock = createMockEServiceProvider({ - activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, + activeDescriptor: { id: 'test-1', state: 'ARCHIVED', version: '1' }, }) const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) expect(result.current.actions).toHaveLength(0) }) - it('should have the correct actions if the user is an api operator and the e-service has an active descriptor in PUBLISHED state and has no version draft', () => { + it('should not return actions if user is an api operator and e-service is DEPRECATED', () => { mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({}) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'DEPRECATED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + it('should not return actions if user is an api operator and delegator, e-service is DEPRECATED', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegator: true }) const descriptorMock = createMockEServiceProvider({ - activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, + activeDescriptor: { id: 'test-1', state: 'DEPRECATED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + + it('should not return actions if user is an api operator and delegate, e-service is DEPRECATED', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'DEPRECATED', version: '1' }, }) const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + it('should return the correct actions if user is an api operator and e-service is PUBLISHED with no draft descriptors', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({}) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) expect(result.current.actions).toHaveLength(2) expect(result.current.actions[0].label).toBe('clone') expect(result.current.actions[1].label).toBe('createNewDraft') }) - it('should have the correct actions if the user is an api operator and the e-service has an active descriptor in PUBLISHED state and has no version draft', () => { + it('should return the correct actions if user is an api operator and e-service is PUBLISHED with a draft descriptor in state DRAFT', () => { mockUseJwt({ isAdmin: false, isOperatorAPI: true }) - + mockUseGetDelegationUserRole({}) const descriptorMock = createMockEServiceProvider({ activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, draftDescriptor: { id: 'test-2', state: 'DRAFT', version: '2' }, @@ -226,19 +524,78 @@ describe('useGetProviderEServiceTableActions tests', () => { expect(result.current.actions[2].label).toBe('deleteDraft') }) - it('should not have any actions if the user is an api operator and the e-service is in DEPRECATED state', () => { + it('should not return actions if user is an api operator and delegator, e-service is PUBLISHED with no draft descriptors', () => { mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegator: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + it('should not return actions if user is an api operator and delegator, e-service is PUBLISHED with a draft descriptor in state DRAFT', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegator: true }) const descriptorMock = createMockEServiceProvider({ - activeDescriptor: { id: 'test-1', state: 'DEPRECATED', version: '1' }, + activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, + draftDescriptor: { id: 'test-2', state: 'DRAFT', version: '2' }, }) const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) expect(result.current.actions).toHaveLength(0) }) - it('should return the correct actions if the user is an api operator and if the e-service has an active descriptor in SUSPENDED state and has no version draft', () => { + it('should return the correct actions if user is an api operator and delegator, e-service is PUBLISHED with a draft descriptor in state WAITING_FOR_APPROVAL', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegator: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, + draftDescriptor: { id: 'test-2', state: 'WAITING_FOR_APPROVAL', version: '2' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(2) + expect(result.current.actions[0].label).toBe('approve') + expect(result.current.actions[1].label).toBe('reject') + }) + + it('should return the correct actions if user is an api operator and delegate, e-service is PUBLISHED with no draft descriptors', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(1) + expect(result.current.actions[0].label).toBe('createNewDraft') + }) + + it('should return the correct actions if user is an api operator and delegate, e-service is PUBLISHED with a draft descriptor in state DRAFT', () => { mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, + draftDescriptor: { id: 'test-2', state: 'DRAFT', version: '2' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(2) + expect(result.current.actions[0].label).toBe('manageDraft') + expect(result.current.actions[1].label).toBe('deleteDraft') + }) + + it('should not return actions if user is an api operator and delegate, e-service is PUBLISHED with a draft descriptor in state WAITING_FOR_APPROVAL', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, + draftDescriptor: { id: 'test-2', state: 'WAITING_FOR_APPROVAL', version: '2' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + it('should return the correct actions if user is an api operator and e-service is SUSPENDED with no draft descriptors', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({}) const descriptorMock = createMockEServiceProvider({ activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, }) @@ -248,9 +605,9 @@ describe('useGetProviderEServiceTableActions tests', () => { expect(result.current.actions[1].label).toBe('createNewDraft') }) - it('should return the correct actions if the user is an api operator and if the e-service has an active descriptor in SUSPENDED state and has a version draft', () => { + it('should return the correct actions if user is an api operator and e-service is SUSPENDED with a draft descriptor in state DRAFT', () => { mockUseJwt({ isAdmin: false, isOperatorAPI: true }) - + mockUseGetDelegationUserRole({}) const descriptorMock = createMockEServiceProvider({ activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, draftDescriptor: { id: 'test-2', state: 'DRAFT', version: '2' }, @@ -261,4 +618,139 @@ describe('useGetProviderEServiceTableActions tests', () => { expect(result.current.actions[1].label).toBe('manageDraft') expect(result.current.actions[2].label).toBe('deleteDraft') }) + + it('should not return actions if user is an api operator and delegator, e-service is SUSPENDED with no draft descriptors', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegator: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + + it('should not return actions if user is an api operator and delegator, e-service is SUSPENDED with a draft descriptor in state DRAFT', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegator: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, + draftDescriptor: { id: 'test-2', state: 'DRAFT', version: '2' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + + it('should return the correct actions if user is an api operator and delegator, e-service is SUSPENDED with a draft descriptor in state WAITING_FOR_APPROVAL', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegator: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, + draftDescriptor: { id: 'test-2', state: 'WAITING_FOR_APPROVAL', version: '2' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(2) + expect(result.current.actions[0].label).toBe('approve') + expect(result.current.actions[1].label).toBe('reject') + }) + + it('should return the correct actions if user is an api operator and delegate, e-service is SUSPENDED with no draft descriptors', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(1) + expect(result.current.actions[0].label).toBe('createNewDraft') + }) + + it('should return the correct actions if user is an api operator and delegate, e-service is SUSPENDED with a draft descriptor in state DRAFT', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, + draftDescriptor: { id: 'test-2', state: 'DRAFT', version: '2' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(2) + expect(result.current.actions[0].label).toBe('manageDraft') + expect(result.current.actions[1].label).toBe('deleteDraft') + }) + + it('should not return actions if user is an api operator and delegate, e-service is SUSPENDED with a draft descriptor in state WAITING_FOR_APPROVAL', () => { + mockUseJwt({ isAdmin: false, isOperatorAPI: true }) + mockUseGetDelegationUserRole({ isDelegate: true }) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, + draftDescriptor: { id: 'test-2', state: 'WAITING_FOR_APPROVAL', version: '2' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) + + it('should navigate to PROVIDE_ESERVICE_EDIT page on clone action success', async () => { + mockUseJwt({ isAdmin: true }) + mockUseGetDelegationUserRole({}) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, + draftDescriptor: { id: 'test-2', state: 'DRAFT', version: '2' }, + }) + const { result, history } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + + const cloneAction = result.current.actions[1] + + expect(cloneAction.label).toBe('clone') + + act(() => { + cloneAction.action() + }) + + act(() => { + fireEvent.click(screen.getByRole('button', { name: 'confirm' })) + }) + + await waitFor(() => { + expect(history.location.pathname).toBe( + '/it/erogazione/e-service/6dbb7416-8315-4970-a6be-393a03d0a79d/fd09a069-81f8-4cb5-a302-64320e83a033/modifica' + ) + }) + }) + + it('should navigate to PROVIDE_ESERVICE_EDIT page on create new draft action success', async () => { + mockUseJwt({ isAdmin: true }) + mockUseGetDelegationUserRole({}) + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'SUSPENDED', version: '1' }, + }) + const { result, history } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + + const cloneAction = result.current.actions[2] + + expect(cloneAction.label).toBe('createNewDraft') + + act(() => { + cloneAction.action() + }) + + act(() => { + fireEvent.click(screen.getByRole('button', { name: 'confirm' })) + }) + + await waitFor(() => { + expect(history.location.pathname).toBe( + '/it/erogazione/e-service/ad474d35-7939-4bee-bde9-4e469cca1030/test-id/modifica' + ) + }) + }) + + it('should not return actions if the user is a security operator', () => { + mockUseGetDelegationUserRole({}) + mockUseJwt({ isAdmin: false, isOperatorSecurity: true }) + + const descriptorMock = createMockEServiceProvider({ + activeDescriptor: { id: 'test-1', state: 'PUBLISHED', version: '1' }, + }) + const { result } = renderUseGetProviderEServiceTableActionsHook(descriptorMock) + expect(result.current.actions).toHaveLength(0) + }) }) diff --git a/src/hooks/useGetAgreementsActions.ts b/src/hooks/useGetAgreementsActions.ts index 71dd39599..02cf5f7ba 100644 --- a/src/hooks/useGetAgreementsActions.ts +++ b/src/hooks/useGetAgreementsActions.ts @@ -11,6 +11,8 @@ import CloseIcon from '@mui/icons-material/Close' import ContentCopyIcon from '@mui/icons-material/ContentCopy' import ArchiveIcon from '@mui/icons-material/Archive' import { AuthHooks } from '@/api/auth' +import { useQuery } from '@tanstack/react-query' +import { DelegationQueries } from '@/api/delegation' type AgreementActions = Record> @@ -19,7 +21,7 @@ function useGetAgreementsActions(agreement?: Agreement | AgreementListEntry): { } { const { t } = useTranslation('common', { keyPrefix: 'actions' }) const { mode, routeKey } = useCurrentRoute() - const { isAdmin } = AuthHooks.useJwt() + const { isAdmin, jwt } = AuthHooks.useJwt() const { openDialog } = useDialog() const navigate = useNavigate() @@ -29,8 +31,23 @@ function useGetAgreementsActions(agreement?: Agreement | AgreementListEntry): { const { mutate: cloneAgreement } = AgreementMutations.useClone() const { mutate: archiveAgreement } = AgreementMutations.useArchive() + const { data: activeProducerDelegation } = useQuery({ + ...DelegationQueries.getProducerDelegationsList({ + limit: 50, + offset: 0, + eserviceIds: [agreement?.eservice.id as string], + states: ['ACTIVE'], + }), + enabled: !!agreement, + select: (d) => d.results[0], + }) + if (!agreement || mode === null || !isAdmin) return { actions: [] } + const isDelegator = activeProducerDelegation?.delegator.id === jwt?.organizationId + + if (isDelegator) return { actions: [] } + const handleActivate = () => { activateAgreement({ agreementId: agreement.id }) } diff --git a/src/hooks/useGetDelegationActions.ts b/src/hooks/useGetDelegationActions.ts new file mode 100644 index 000000000..9440df721 --- /dev/null +++ b/src/hooks/useGetDelegationActions.ts @@ -0,0 +1,74 @@ +import type { CompactDelegation, Delegation } from '@/api/api.generatedTypes' +import { useDialog } from '@/stores' +import type { ActionItemButton } from '@/types/common.types' +import { useTranslation } from 'react-i18next' +import GradingIcon from '@mui/icons-material/Grading' +import CloseIcon from '@mui/icons-material/Close' +import { AuthHooks } from '@/api/auth' + +export function useGetDelegationActions(delegation: Delegation | CompactDelegation | undefined) { + const { t: tCommon } = useTranslation('common', { keyPrefix: 'actions' }) + const { openDialog } = useDialog() + const { jwt } = AuthHooks.useJwt() + + const actions: Array = [] + + if (!delegation) return { actions: actions } + + const handleAccept = () => { + openDialog({ type: 'acceptDelegation', delegationId: delegation.id }) + } + + const acceptAction: ActionItemButton = { + action: handleAccept, + label: tCommon('accept'), + color: 'primary', + icon: GradingIcon, + } + + const handleReject = () => { + openDialog({ type: 'rejectDelegation', delegationId: delegation.id }) + } + + const rejectAction: ActionItemButton = { + action: handleReject, + label: tCommon('reject'), + color: 'error', + icon: CloseIcon, + } + + const handleRevoke = () => { + openDialog({ + type: 'revokeProducerDelegation', + delegationId: delegation.id, + eserviceName: delegation.eservice?.name ?? '-', + }) + } + + const revokeAction: ActionItemButton = { + action: handleRevoke, + label: tCommon('revoke'), + color: 'error', + icon: CloseIcon, + } + + if ( + delegation.kind === 'DELEGATED_PRODUCER' && + delegation.state === 'WAITING_FOR_APPROVAL' && + delegation.delegate.id === jwt?.organizationId + ) { + actions.push(...[acceptAction, rejectAction]) + } + + if ( + delegation.kind === 'DELEGATED_PRODUCER' && + delegation.state === 'ACTIVE' && + delegation.delegator.id === jwt?.organizationId + ) { + actions.push(revokeAction) + } + + return { + actions: actions, + } +} diff --git a/src/hooks/useGetDelegationUserRole.ts b/src/hooks/useGetDelegationUserRole.ts new file mode 100644 index 000000000..a756f09b9 --- /dev/null +++ b/src/hooks/useGetDelegationUserRole.ts @@ -0,0 +1,37 @@ +import { DelegationQueries } from '@/api/delegation' +import { useQuery } from '@tanstack/react-query' + +export function useGetDelegationUserRole({ + eserviceId, + organizationId, +}: { + eserviceId: string + organizationId: string | undefined +}) { + const { data: producerDelegations } = useQuery({ + ...DelegationQueries.getProducerDelegationsList({ + eserviceIds: [eserviceId], + states: ['ACTIVE'], + kind: 'DELEGATED_PRODUCER', + offset: 0, + limit: 50, + }), + select: (delegations) => delegations.results, + }) + + const isDelegate = Boolean( + organizationId && + producerDelegations?.find((delegation) => delegation.delegate.id === organizationId) + ) + + const isDelegator = Boolean( + organizationId && + producerDelegations?.find((delegation) => delegation.delegator.id === organizationId) + ) + + return { + isDelegate, + isDelegator, + producerDelegations, + } +} diff --git a/src/hooks/useGetProviderEServiceActions.ts b/src/hooks/useGetProviderEServiceActions.ts index 6c9e9db10..1d87bce3f 100644 --- a/src/hooks/useGetProviderEServiceActions.ts +++ b/src/hooks/useGetProviderEServiceActions.ts @@ -11,31 +11,55 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy' import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline' import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline' import PendingActionsIcon from '@mui/icons-material/PendingActions' +import PublishIcon from '@mui/icons-material/Publish' +import { useDialog } from '@/stores' +import { useGetDelegationUserRole } from './useGetDelegationUserRole' +import { match } from 'ts-pattern' export function useGetProviderEServiceActions( - eserviceId: string | undefined, + eserviceId: string, descriptorState: EServiceDescriptorState | undefined, + draftDescriptorState: EServiceDescriptorState | undefined, activeDescriptorId: string | undefined, draftDescriptorId: string | undefined, mode: EServiceMode | undefined ): { actions: Array } { const { t } = useTranslation('common', { keyPrefix: 'actions' }) - const { isAdmin, isOperatorAPI } = AuthHooks.useJwt() + const { t: tDialogApproveDelegatedVersionDraft } = useTranslation('shared-components', { + keyPrefix: 'dialogApproveDelegatedVersionDraft', + }) + const { isAdmin, isOperatorAPI, jwt } = AuthHooks.useJwt() const navigate = useNavigate() + const { openDialog, closeDialog } = useDialog() - const { mutate: publishDraft } = EServiceMutations.usePublishVersionDraft() + const { isDelegator, isDelegate, producerDelegations } = useGetDelegationUserRole({ + eserviceId, + organizationId: jwt?.organizationId, + }) + + const delegation = producerDelegations?.find( + (delegation) => delegation.eservice?.id === eserviceId + ) + + const { mutate: publishDraft } = EServiceMutations.usePublishVersionDraft({ + isByDelegation: isDelegate, + }) const { mutate: deleteDraft } = EServiceMutations.useDeleteDraft() const { mutate: deleteVersionDraft } = EServiceMutations.useDeleteVersionDraft() const { mutate: suspend } = EServiceMutations.useSuspendVersion() const { mutate: reactivate } = EServiceMutations.useReactivateVersion() const { mutate: clone } = EServiceMutations.useCloneFromVersion() const { mutate: createNewDraft } = EServiceMutations.useCreateVersionDraft() + const { mutate: approveDelegatedVersionDraft } = + EServiceMutations.useApproveDelegatedVersionDraft() - const state = descriptorState ?? 'DRAFT' + const state = descriptorState ?? draftDescriptorState ?? 'DRAFT' const hasVersionDraft = !!draftDescriptorId + const isDraftWaitingForApproval = draftDescriptorState === 'WAITING_FOR_APPROVAL' + // Only admin and operatorAPI can see actions - if (!eserviceId || (!isAdmin && !isOperatorAPI)) return { actions: [] } + if (!isAdmin && !isOperatorAPI) return { actions: [] } const deleteDraftAction: ActionItemButton = { action: deleteDraft.bind(null, { eserviceId }), @@ -45,7 +69,13 @@ export function useGetProviderEServiceActions( } const handlePublishDraft = () => { - if (draftDescriptorId) publishDraft({ eserviceId, descriptorId: draftDescriptorId }) + if (draftDescriptorId) + publishDraft({ + eserviceId, + descriptorId: draftDescriptorId, + delegatorName: delegation?.delegator.name, + eserviceName: delegation?.eservice?.name, + }) } const publishDraftAction: ActionItemButton = { @@ -143,36 +173,319 @@ export function useGetProviderEServiceActions( icon: PendingActionsIcon, } + const handleRejectDelegatedVersionDraft = () => { + if (draftDescriptorId) { + openDialog({ + type: 'rejectDelegatedVersionDraft', + eserviceId, + descriptorId: draftDescriptorId, + }) + } + } + + const rejectDelegatedVersionDraftAction: ActionItemButton = { + action: handleRejectDelegatedVersionDraft, + label: t('reject'), + icon: DeleteOutlineIcon, + color: 'error', + } + + const handleApproveDelegatedVersionDraft = () => { + if (draftDescriptorId) { + const handleProceed = () => { + approveDelegatedVersionDraft({ eserviceId, descriptorId: draftDescriptorId }) + closeDialog() + } + + openDialog({ + type: 'basic', + title: tDialogApproveDelegatedVersionDraft('title'), + description: tDialogApproveDelegatedVersionDraft('description', { + eserviceName: delegation?.eservice?.name, + delegateName: delegation?.delegate.name, + }), + proceedLabel: tDialogApproveDelegatedVersionDraft('actions.approveAndPublish'), + onProceed: handleProceed, + }) + } + } + + const approveDelegatedVersionDraftAction: ActionItemButton = { + action: handleApproveDelegatedVersionDraft, + label: t('approve'), + icon: PublishIcon, + } + const deleteAction = !activeDescriptorId ? deleteDraftAction : deleteVersionDraftAction - const adminActions: Record> = { - PUBLISHED: [ + const publishedActions = match({ + isAdmin, + isDelegator, + isDelegate, + hasVersionDraft, + isDraftWaitingForApproval, + }) + .with({ isAdmin: true, isDelegator: false, isDelegate: false, hasVersionDraft: false }, () => [ cloneAction, - ...(!hasVersionDraft ? [createNewDraftAction] : [editDraftAction, deleteAction]), + createNewDraftAction, suspendAction, - ], - ARCHIVED: [], - DEPRECATED: [suspendAction], - DRAFT: !hasVersionDraft ? [deleteAction] : [publishDraftAction, deleteAction], - SUSPENDED: [ + ]) + .with({ isAdmin: true, isDelegator: false, isDelegate: false, hasVersionDraft: true }, () => [ + cloneAction, + editDraftAction, + deleteAction, + suspendAction, + ]) + .with({ isAdmin: true, isDelegator: true, isDelegate: false, hasVersionDraft: false }, () => []) + .with( + { + isAdmin: true, + isDelegator: true, + isDelegate: false, + hasVersionDraft: true, + isDraftWaitingForApproval: false, + }, + () => [] + ) + .with( + { + isAdmin: true, + isDelegator: true, + isDelegate: false, + hasVersionDraft: true, + isDraftWaitingForApproval: true, + }, + () => [approveDelegatedVersionDraftAction, rejectDelegatedVersionDraftAction] + ) + .with({ isAdmin: true, isDelegator: false, isDelegate: true, hasVersionDraft: false }, () => [ + createNewDraftAction, + suspendAction, + ]) + .with( + { + isAdmin: true, + isDelegator: false, + isDelegate: true, + hasVersionDraft: true, + isDraftWaitingForApproval: false, + }, + () => [editDraftAction, deleteAction, suspendAction] + ) + .with( + { + isAdmin: true, + isDelegator: false, + isDelegate: true, + hasVersionDraft: true, + isDraftWaitingForApproval: true, + }, + () => [suspendAction] + ) + .with({ isAdmin: false, isDelegator: false, isDelegate: false, hasVersionDraft: false }, () => [ + cloneAction, + createNewDraftAction, + ]) + .with({ isAdmin: false, isDelegator: false, isDelegate: false, hasVersionDraft: true }, () => [ + cloneAction, + editDraftAction, + deleteAction, + ]) + .with( + { isAdmin: false, isDelegator: true, isDelegate: false, hasVersionDraft: false }, + () => [] + ) + .with( + { + isAdmin: false, + isDelegator: true, + isDelegate: false, + hasVersionDraft: true, + isDraftWaitingForApproval: false, + }, + () => [] + ) + .with( + { + isAdmin: false, + isDelegator: true, + isDelegate: false, + hasVersionDraft: true, + isDraftWaitingForApproval: true, + }, + () => [approveDelegatedVersionDraftAction, rejectDelegatedVersionDraftAction] + ) + .with({ isAdmin: false, isDelegator: false, isDelegate: true, hasVersionDraft: false }, () => [ + createNewDraftAction, + ]) + .with( + { + isAdmin: false, + isDelegator: false, + isDelegate: true, + hasVersionDraft: true, + isDraftWaitingForApproval: false, + }, + () => [editDraftAction, deleteAction] + ) + .with( + { + isAdmin: false, + isDelegator: false, + isDelegate: true, + hasVersionDraft: true, + isDraftWaitingForApproval: true, + }, + () => [] + ) + .otherwise(() => []) + + const draftActions = match({ isDelegator, isDelegate }) + .with({ isDelegator: false, isDelegate: false }, () => [publishDraftAction, deleteAction]) + .with({ isDelegator: true, isDelegate: false }, () => []) + .with({ isDelegator: false, isDelegate: true }, () => [publishDraftAction]) + .otherwise(() => []) + + const suspendedActions = match({ + isAdmin, + isDelegator, + isDelegate, + hasVersionDraft, + isDraftWaitingForApproval, + }) + .with({ isAdmin: true, isDelegator: false, isDelegate: false, hasVersionDraft: false }, () => [ reactivateAction, cloneAction, - ...(!hasVersionDraft ? [createNewDraftAction] : [editDraftAction, deleteAction]), - ], + createNewDraftAction, + ]) + .with({ isAdmin: true, isDelegator: false, isDelegate: false, hasVersionDraft: true }, () => [ + reactivateAction, + cloneAction, + editDraftAction, + deleteAction, + ]) + .with({ isAdmin: true, isDelegator: true, isDelegate: false, hasVersionDraft: false }, () => []) + .with( + { + isAdmin: true, + isDelegator: true, + isDelegate: false, + hasVersionDraft: true, + isDraftWaitingForApproval: false, + }, + () => [] + ) + .with( + { + isAdmin: true, + isDelegator: true, + isDelegate: false, + hasVersionDraft: true, + isDraftWaitingForApproval: true, + }, + () => [approveDelegatedVersionDraftAction, rejectDelegatedVersionDraftAction] + ) + .with({ isAdmin: true, isDelegator: false, isDelegate: true, hasVersionDraft: false }, () => [ + reactivateAction, + createNewDraftAction, + ]) + .with( + { + isAdmin: true, + isDelegator: false, + isDelegate: true, + hasVersionDraft: true, + isDraftWaitingForApproval: false, + }, + () => [reactivateAction, editDraftAction, deleteAction] + ) + .with( + { + isAdmin: true, + isDelegator: false, + isDelegate: true, + hasVersionDraft: true, + isDraftWaitingForApproval: true, + }, + () => [reactivateAction] + ) + .with({ isAdmin: false, isDelegator: false, isDelegate: false, hasVersionDraft: false }, () => [ + cloneAction, + createNewDraftAction, + ]) + .with({ isAdmin: false, isDelegator: false, isDelegate: false, hasVersionDraft: true }, () => [ + cloneAction, + editDraftAction, + deleteAction, + ]) + .with( + { isAdmin: false, isDelegator: true, isDelegate: false, hasVersionDraft: false }, + () => [] + ) + .with( + { + isAdmin: false, + isDelegator: true, + isDelegate: false, + hasVersionDraft: true, + isDraftWaitingForApproval: false, + }, + () => [] + ) + .with( + { + isAdmin: false, + isDelegator: true, + isDelegate: false, + hasVersionDraft: true, + isDraftWaitingForApproval: true, + }, + () => [approveDelegatedVersionDraftAction, rejectDelegatedVersionDraftAction] + ) + .with({ isAdmin: false, isDelegator: false, isDelegate: true, hasVersionDraft: false }, () => [ + createNewDraftAction, + ]) + .with( + { + isAdmin: false, + isDelegator: false, + isDelegate: true, + hasVersionDraft: true, + isDraftWaitingForApproval: false, + }, + () => [editDraftAction, deleteAction] + ) + .with( + { + isAdmin: false, + isDelegator: false, + isDelegate: true, + hasVersionDraft: true, + isDraftWaitingForApproval: true, + }, + () => [] + ) + .otherwise(() => []) + + const adminActions: Record> = { + PUBLISHED: publishedActions, + ARCHIVED: [], + DEPRECATED: isDelegator ? [] : [suspendAction], + DRAFT: draftActions, + SUSPENDED: suspendedActions, + WAITING_FOR_APPROVAL: isDelegator + ? [approveDelegatedVersionDraftAction, rejectDelegatedVersionDraftAction] + : [], } const operatorAPIActions: Record> = { - PUBLISHED: [ - cloneAction, - ...(!hasVersionDraft ? [createNewDraftAction] : [editDraftAction, deleteAction]), - ], + PUBLISHED: publishedActions, ARCHIVED: [], DEPRECATED: [], - DRAFT: !hasVersionDraft ? [deleteAction] : [publishDraftAction, deleteAction], - SUSPENDED: [ - cloneAction, - ...(!hasVersionDraft ? [createNewDraftAction] : [editDraftAction, deleteAction]), - ], + DRAFT: draftActions, + SUSPENDED: suspendedActions, + WAITING_FOR_APPROVAL: isDelegator + ? [approveDelegatedVersionDraftAction, rejectDelegatedVersionDraftAction] + : [], } const availableAction = isAdmin ? adminActions[state] : operatorAPIActions[state] diff --git a/src/pages/DelegationCreatePage/DelegationCreate.page.tsx b/src/pages/DelegationCreatePage/DelegationCreate.page.tsx new file mode 100644 index 000000000..afd928d65 --- /dev/null +++ b/src/pages/DelegationCreatePage/DelegationCreate.page.tsx @@ -0,0 +1,67 @@ +import { PageContainer, SectionContainer } from '@/components/layout/containers' +import { useNavigate } from '@/router' +import { Stack } from '@mui/material' +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { DelegationCreateCards } from './components/DelegationCreateCards' +import { StepActions } from '@/components/shared/StepActions' +import ArrowForwardIcon from '@mui/icons-material/ArrowForward' +import { DelegationCreateForm } from './components/DelegationCreateForm' +import type { DelegationKind } from '@/api/api.generatedTypes' + +/*** + * It shows the cards component to choose delegation kind or the form component based on the state KIND or FORM + */ +export const DelegationCreatePage: React.FC = () => { + const { t } = useTranslation('party') + const navigate = useNavigate() + const [delegationKind, setDelegationKind] = useState() + const [activeStep, setActiveStep] = useState<'KIND' | 'FORM'>('KIND') + + const changeDelegationKind = (delegationKind: DelegationKind) => { + setDelegationKind(delegationKind) + } + + return ( + + + {activeStep === 'KIND' && ( + + + + + + )} + {activeStep === 'FORM' && delegationKind != null && ( + + )} + + {activeStep === 'KIND' && ( + navigate('DELEGATIONS'), + }} + forward={{ + label: t('delegations.create.forwardWithSaveBtn'), + type: 'button', + endIcon: , + onClick: () => { + delegationKind != null && setActiveStep('FORM') + }, + }} + /> + )} + + ) +} diff --git a/src/pages/DelegationCreatePage/components/DelegationCreateCards.tsx b/src/pages/DelegationCreatePage/components/DelegationCreateCards.tsx new file mode 100644 index 000000000..e903b2e5e --- /dev/null +++ b/src/pages/DelegationCreatePage/components/DelegationCreateCards.tsx @@ -0,0 +1,154 @@ +import React from 'react' +import type { DelegationKind } from '@/api/api.generatedTypes' +import type { SxProps } from '@mui/material' +import { + Button, + Card, + CardContent, + FormControl, + FormControlLabel, + Radio, + RadioGroup, + Typography, +} from '@mui/material' + +import { useTranslation } from 'react-i18next' + +type DelegationCreateCardsProps = { + selectedDelegationKind: DelegationKind | undefined + changeDelegationKind: (delegationKind: DelegationKind) => void +} + +export const DelegationCreateCards: React.FC = ({ + selectedDelegationKind, + changeDelegationKind, +}) => { + const { t } = useTranslation('party') + + const consumerDelegated: DelegationKind = 'DELEGATED_CONSUMER' + const producerDelegated: DelegationKind = 'DELEGATED_PRODUCER' + + const svgCardIcon = ( + + + + + + ) + + const getSxProps = (delegationKind: DelegationKind): SxProps => ({ + width: '100%', + height: '100%', + display: 'flex', + maxHeight: 97, + border: 2, + boxShadow: 2, + borderColor: 'primary.main', + backgroundColor: selectedDelegationKind === delegationKind ? 'primary.dark' : 'white', + '& .MuiTypography-root': { + color: selectedDelegationKind === delegationKind ? 'white' : 'primary.main', + }, + '& svg path': { + fill: selectedDelegationKind === delegationKind ? 'white' : 'primary.main', + }, + '&:hover': { + backgroundColor: 'primary.dark', + '& .MuiTypography-root': { + color: 'white', + transition: 'color 250ms cubic-bezier(0.4, 0, 0.2, 1)', + }, + '& svg path': { + fill: 'white', + transition: 'color 250ms cubic-bezier(0.4, 0, 0.2, 1)', + }, + }, + }) + + return ( + <> + + + } + label={ + changeDelegationKind(consumerDelegated)} + sx={getSxProps('DELEGATED_CONSUMER')} + > + + {svgCardIcon} +
+ + {t('delegations.create.cards.common')} + + + {t('delegations.create.cards.consume')} + +
+
+
+ } + /> + } + label={ + changeDelegationKind(producerDelegated)} + sx={getSxProps('DELEGATED_PRODUCER')} + > + + {svgCardIcon} +
+ + {t('delegations.create.cards.common')} + + + {t('delegations.create.cards.provide')} + +
+
+
+ } + /> +
+
+ + ) +} diff --git a/src/pages/DelegationCreatePage/components/DelegationCreateForm.tsx b/src/pages/DelegationCreatePage/components/DelegationCreateForm.tsx new file mode 100644 index 000000000..2a76f3966 --- /dev/null +++ b/src/pages/DelegationCreatePage/components/DelegationCreateForm.tsx @@ -0,0 +1,214 @@ +import { RHFAutocompleteSingle, RHFTextField } from '@/components/shared/react-hook-form-inputs' +import { Box, FormControlLabel, Stack, Switch } from '@mui/material' +import React, { useState } from 'react' +import { FormProvider, useForm } from 'react-hook-form' +import { useTranslation } from 'react-i18next' +import { EServiceQueries } from '@/api/eservice/eservice.queries' +import { useQuery } from '@tanstack/react-query' +import type { DelegationKind } from '@/api/api.generatedTypes' +import { SectionContainer } from '@/components/layout/containers' +import { useDialog } from '@/stores' +import { TenantQueries } from '@/api/tenant' +import { DelegationMutations } from '@/api/delegation' +import { useNavigate } from '@/router' +import { StepActions } from '@/components/shared/StepActions' +import ArrowBackIcon from '@mui/icons-material/ArrowBack' +import SendIcon from '@mui/icons-material/Send' +import { AuthHooks } from '@/api/auth' + +export type DelegationCreateFormValues = { + eserviceName: string + eserviceDescription: string + delegateId: string +} + +type DelegationCreateFormProps = { + delegationKind: DelegationKind + setActiveStep: React.Dispatch> +} + +const defaultValues: DelegationCreateFormValues = { + eserviceName: '', + eserviceDescription: '', + delegateId: '', +} + +export const DelegationCreateForm: React.FC = ({ + delegationKind, + setActiveStep, +}) => { + const { t } = useTranslation('party') + + const [isExistingEservice, setIsExistingEservice] = useState(false) + + const currentUserOrganizationId = AuthHooks.useJwt()?.jwt?.organizationId + const { openDialog } = useDialog() + + const formMethods = useForm({ defaultValues }) + + const { data: autocompleteEserviceOptions = [], isLoading: isLoadingEservices } = useQuery({ + ...EServiceQueries.getProviderList({ + limit: 50, + offset: 0, + delegated: false, + }), + select: (d) => + (d.results ?? []).map((eservice) => ({ + label: eservice.name, + value: eservice.id, + })), + }) + + const { data: autocompleteDelegateOptions = [], isLoading: isLoadingDelegates } = useQuery({ + ...TenantQueries.getTenants({ + limit: 50, + features: ['DELEGATED_PRODUCER'], + }), + select: (d) => + (d.results ?? []) + .filter((d) => d.id !== currentUserOrganizationId) + .map((delegate) => ({ + label: delegate.name, + value: delegate.id, + })), + }) + + const { mutate: createProducerDelegation } = DelegationMutations.useCreateProducerDelegation() + const { mutate: createProducerDelegationAndEservice } = + DelegationMutations.useCreateProducerDelegationAndEservice() + + const onSubmit = async (formValues: DelegationCreateFormValues) => { + openDialog({ + type: 'delegations', + onConfirm: () => onConfirm(formValues), + }) + } + + const navigate = useNavigate() + + function onConfirm(formValues: DelegationCreateFormValues) { + if (!isExistingEservice && delegationKind === 'DELEGATED_PRODUCER') { + // if it is a producer delegation and isExistingEservice is false the eservice must be created + createProducerDelegationAndEservice( + { + name: formValues.eserviceName, + description: formValues.eserviceDescription, + technology: 'REST', + mode: 'DELIVER', + delegateId: formValues.delegateId, + }, + { + onSuccess: () => { + navigate('DELEGATIONS') + }, + } + ) + } else { + const createDelegationParams = { + eserviceId: formValues.eserviceName, + delegateId: formValues.delegateId, + } + console.log({ createDelegationParams }) + + createProducerDelegation(createDelegationParams, { + onSuccess: () => { + navigate('DELEGATIONS') + }, + }) + } + } + + const sectionTitle = + delegationKind === 'DELEGATED_PRODUCER' + ? t('delegations.create.provideDelegationTitle') + : t('delegations.create.consumeDelegationTitle') + + return ( + + + + + {delegationKind === 'DELEGATED_PRODUCER' && ( + setIsExistingEservice((prev) => !prev)} + /> + } + label={t('delegations.create.delegateField.provide.switch')} + labelPlacement="end" + componentsProps={{ typography: { variant: 'body2' } }} + /> + )} + {isExistingEservice || delegationKind === 'DELEGATED_CONSUMER' ? ( + + ) : ( + <> + + + + )} + + + + + , + onClick: () => { + setActiveStep('KIND') + }, + }} + forward={{ + label: t('delegations.create.submitBtn'), + type: 'submit', + startIcon: , + }} + /> + + ) +} diff --git a/src/pages/DelegationCreatePage/index.ts b/src/pages/DelegationCreatePage/index.ts new file mode 100644 index 000000000..4fb255cc0 --- /dev/null +++ b/src/pages/DelegationCreatePage/index.ts @@ -0,0 +1 @@ +export { DelegationCreatePage } from './DelegationCreate.page' diff --git a/src/pages/DelegationDetailsPage/DelegationDetails.page.tsx b/src/pages/DelegationDetailsPage/DelegationDetails.page.tsx new file mode 100644 index 000000000..cff340250 --- /dev/null +++ b/src/pages/DelegationDetailsPage/DelegationDetails.page.tsx @@ -0,0 +1,69 @@ +import { DelegationQueries } from '@/api/delegation' +import { PageContainer } from '@/components/layout/containers' +import { useParams } from '@/router' +import { useQuery } from '@tanstack/react-query' +import React from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { + DelegationGeneralInfoSection, + DelegationGeneralInfoSectionSkeleton, +} from './components/DelegationGeneralInfoSection' +import { Alert, Link } from '@mui/material' +import { useDrawerState } from '@/hooks/useDrawerState' +import { RejectReasonDrawer } from '@/components/shared/RejectReasonDrawer' +import { useGetDelegationActions } from '@/hooks/useGetDelegationActions' + +export const DelegationDetailsPage: React.FC = () => { + const { delegationId } = useParams<'DELEGATION_DETAILS'>() + const { t } = useTranslation('party', { keyPrefix: 'delegations.details' }) + + const { data: delegation, isLoading } = useQuery( + DelegationQueries.getSingle({ delegationId: delegationId }) + ) + + const { isOpen, openDrawer, closeDrawer } = useDrawerState() + + const { actions } = useGetDelegationActions(delegation) + + return ( + + {delegation && delegation.state === 'REJECTED' && delegation.rejectionReason && ( + + + ), + }} + > + {t('rejectedDelegationAlert')} + + + )} + }> + + + {delegation && delegation.rejectionReason && ( + + )} + + ) +} diff --git a/src/pages/DelegationDetailsPage/components/DelegationGeneralInfoSection.tsx b/src/pages/DelegationDetailsPage/components/DelegationGeneralInfoSection.tsx new file mode 100644 index 000000000..e2c61e239 --- /dev/null +++ b/src/pages/DelegationDetailsPage/components/DelegationGeneralInfoSection.tsx @@ -0,0 +1,190 @@ +import { AuthHooks } from '@/api/auth' +import { DelegationDownloads, DelegationQueries } from '@/api/delegation' +import { SectionContainer, SectionContainerSkeleton } from '@/components/layout/containers' +import { StatusChip } from '@/components/shared/StatusChip' +import { Link } from '@/router' +import { getLastDescriptor } from '@/utils/eservice.utils' +import { formatDateString } from '@/utils/format.utils' +import { Grid, Stack } from '@mui/material' +import { InformationContainer } from '@pagopa/interop-fe-commons' +import { useSuspenseQuery } from '@tanstack/react-query' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { match, P } from 'ts-pattern' +import DownloadIcon from '@mui/icons-material/Download' + +type DelegationGeneralInfoSectionProps = { + delegationId: string +} + +export const DelegationGeneralInfoSection: React.FC = ({ + delegationId, +}) => { + const { t } = useTranslation('party', { keyPrefix: 'delegations.details.generalInfoSection' }) + const { data: delegation } = useSuspenseQuery( + DelegationQueries.getSingle({ delegationId: delegationId }) + ) + + const { jwt } = AuthHooks.useJwt() + + const delegationKindLabel = match(delegation.kind) + .with('DELEGATED_PRODUCER', () => t('delegationKindField.kindProducer')) + .with('DELEGATED_CONSUMER', () => t('delegationKindField.kindConsumer')) + .exhaustive() + + const isUserDelegate = match(jwt?.organizationId) + .with(delegation.delegate.id, () => true) + .with(delegation.delegator.id, () => false) + .otherwise(() => false) + + const lastDescriptor = getLastDescriptor(delegation.eservice.descriptors) + + const downloadDelegationContract = DelegationDownloads.useDownloadDelegationContract() + + const handleDownloadDelegationDocument = () => { + if (!delegation.activationContract) return + downloadDelegationContract( + { + delegationId: delegationId, + contractId: delegation.activationContract?.id, + }, + `${delegation.activationContract.prettyName}.pdf` + ) + } + + const handleDownloadRevokeDelegationDocument = () => { + if (!delegation.revocationContract) return + downloadDelegationContract( + { + delegationId: delegationId, + contractId: delegation.revocationContract?.id, + }, + `${delegation.revocationContract.prettyName}.pdf` + ) + } + + const downloadDelegationContractAction = { + startIcon: , + label: t('downloadContractAction.label'), + component: 'button', + type: 'button', + onClick: handleDownloadDelegationDocument, + } + + const downloadRevokeDelegationContractAction = { + startIcon: , + label: t('downloadRevokedContractAction.label'), + component: 'button', + type: 'button', + onClick: handleDownloadRevokeDelegationDocument, + } + + const downloadContractActions = match(delegation.state) + .with('ACTIVE', () => [downloadDelegationContractAction]) + .with('REVOKED', () => [ + downloadDelegationContractAction, + downloadRevokeDelegationContractAction, + ]) + .otherwise(() => []) + + return ( + + + + + ( + + {delegation.eservice.name} + + ) + ) + .with( + { lastDescriptor: { state: P.not('DRAFT') }, delegationState: 'ACTIVE' }, + ({ lastDescriptor }) => ( + + {delegation.eservice.name} + + ) + ) + .with( + { + lastDescriptor: { state: P.union('PUBLISHED', 'SUSPENDED') }, + delegationState: P.not('ACTIVE'), + }, + ({ lastDescriptor }) => ( + + {delegation.eservice.name} + + ) + ) + .otherwise(() => delegation.eservice.name)} + /> + + + {isUserDelegate && ( + + )} + {!isUserDelegate && ( + + )} + {delegation.submittedAt && ( + + )} + } + /> + + + + + ) +} + +export const DelegationGeneralInfoSectionSkeleton = () => { + return ( + + + + + + ) +} diff --git a/src/pages/DelegationDetailsPage/index.ts b/src/pages/DelegationDetailsPage/index.ts new file mode 100644 index 000000000..107ec65c9 --- /dev/null +++ b/src/pages/DelegationDetailsPage/index.ts @@ -0,0 +1 @@ +export { DelegationDetailsPage } from './DelegationDetails.page' diff --git a/src/pages/DelegationsPage/Delegations.page.tsx b/src/pages/DelegationsPage/Delegations.page.tsx new file mode 100644 index 000000000..89664f4e2 --- /dev/null +++ b/src/pages/DelegationsPage/Delegations.page.tsx @@ -0,0 +1,44 @@ +import { PageContainer } from '@/components/layout/containers' +import { useActiveTab } from '@/hooks/useActiveTab' +import { TabContext, TabList, TabPanel } from '@mui/lab' +import { Tab } from '@mui/material' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { DelegationsGrantedTab } from './DelegationsGrantedTab/DelegationsGrantedTab' +import { DelegationsReceivedTab } from './components/DelegationsReceivedTab/DelegationsReceivedTab' +import { DelegationsAvailabilityTab } from './DelegationsAvailabilityTab/DelegationAvailabilityTab' + +export const DelegationsPage: React.FC = () => { + const { t: tPages } = useTranslation('pages', { keyPrefix: 'delegations' }) + const { t } = useTranslation('party', { keyPrefix: 'delegations' }) + const { activeTab, updateActiveTab } = useActiveTab('delegationsGranted') + + return ( + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/pages/DelegationsPage/DelegationsAvailabilityTab/DelegationAvailabilityDrawer.tsx b/src/pages/DelegationsPage/DelegationsAvailabilityTab/DelegationAvailabilityDrawer.tsx new file mode 100644 index 000000000..1a7e56209 --- /dev/null +++ b/src/pages/DelegationsPage/DelegationsAvailabilityTab/DelegationAvailabilityDrawer.tsx @@ -0,0 +1,97 @@ +import React from 'react' +import { Drawer } from '@/components/shared/Drawer' +import { Box, FormControlLabel, Stack, Switch, Typography } from '@mui/material' +import { useTranslation } from 'react-i18next' +import { SectionContainer } from '@/components/layout/containers' +import { TenantMutations } from '@/api/tenant' + +type DelegationAvailabilityDrawerProps = { + isOpen: boolean + onClose: VoidFunction + isAvailableProducerDelegations: boolean +} + +export const DelegationAvailabilityDrawer: React.FC = ({ + isOpen, + onClose, + isAvailableProducerDelegations, +}) => { + const { t } = useTranslation('party', { keyPrefix: 'delegations.availabilityTab' }) + const { t: tCommon } = useTranslation('shared-components') + const { mutate: assignProducerDelegationAvailabilty } = + TenantMutations.useAssignTenantDelegatedProducerFeature() + const { mutate: deleteTenantDelegatedProducerFeature } = + TenantMutations.useDeleteTenantDelegatedProducerFeature() + + const [checkedProducerDelegations, setCheckedProducerDelegations] = React.useState( + isAvailableProducerDelegations + ) + + const checkedConsumerDelegations = false //TODO disponibilità fruizione + + function handleClick() { + if (checkedProducerDelegations != isAvailableProducerDelegations) { + if (checkedProducerDelegations === true) { + assignProducerDelegationAvailabilty() + } else { + deleteTenantDelegatedProducerFeature() + } + } + onClose() + } + + return ( + + + + + + {t('consumeDelegation.label')} + {t('consumeDelegation.infoLabel')} + { //TODO + setCheckedConsumerDelegations(!checkedConsumerDelegations) + }}*/ + /> + } + label={t('consumeDelegation.value.true')} + labelPlacement="end" + componentsProps={{ typography: { variant: 'body2' } }} + /> + + + + + {t('produceDelegation.label')} + {t('produceDelegation.infoLabel')} + { + setCheckedProducerDelegations(!checkedProducerDelegations) + }} + /> + } + label={t('produceDelegation.value.true')} + labelPlacement="end" + componentsProps={{ typography: { variant: 'body2' } }} + /> + + + + + + ) +} diff --git a/src/pages/DelegationsPage/DelegationsAvailabilityTab/DelegationAvailabilityTab.tsx b/src/pages/DelegationsPage/DelegationsAvailabilityTab/DelegationAvailabilityTab.tsx new file mode 100644 index 000000000..9372d98c2 --- /dev/null +++ b/src/pages/DelegationsPage/DelegationsAvailabilityTab/DelegationAvailabilityTab.tsx @@ -0,0 +1,89 @@ +import React from 'react' +import { SectionContainer, SectionContainerSkeleton } from '@/components/layout/containers' +import { useTranslation } from 'react-i18next' +import { Grid, Stack } from '@mui/material' +import { InformationContainer } from '@pagopa/interop-fe-commons' +import { AuthHooks } from '@/api/auth' +import EditIcon from '@mui/icons-material/Edit' +import { DelegationAvailabilityDrawer } from './DelegationAvailabilityDrawer' +import { TenantHooks } from '@/api/tenant' +import { hasTenantGivenProducerDelegationAvailability } from '@/utils/tenant.utils' + +export const DelegationsAvailabilityTab: React.FC = () => { + const { t } = useTranslation('party', { keyPrefix: 'delegations.availabilityTab' }) + const { t: tCommon } = useTranslation('common') + const { isAdmin } = AuthHooks.useJwt() + + const { data: activeTenant } = TenantHooks.useGetActiveUserParty() + const producerDelegationsAvailability = hasTenantGivenProducerDelegationAvailability(activeTenant) + + const isAvailableProducerDelegations = Boolean(producerDelegationsAvailability) + + const [isAvailableConsumerDelegations, setIsAvailableConsumerDelegations] = React.useState(false) //TODO integrare con BE + + const [isDrawerOpen, setIsDrawerOpen] = React.useState(false) + + const handleOpenDrawer = () => { + setIsDrawerOpen(true) + } + + const onCloseDrawer = () => { + setIsDrawerOpen(false) + } + + return ( + + + + + + + + + + + + + + + + ) +} + +export const DelegationsAvailabilitySectionSkeleton: React.FC = () => { + return ( + + + + + + ) +} diff --git a/src/pages/DelegationsPage/DelegationsGrantedTab/DelegationsGrantedTab.tsx b/src/pages/DelegationsPage/DelegationsGrantedTab/DelegationsGrantedTab.tsx new file mode 100644 index 000000000..741012c3b --- /dev/null +++ b/src/pages/DelegationsPage/DelegationsGrantedTab/DelegationsGrantedTab.tsx @@ -0,0 +1,54 @@ +import type { GetDelegationsParams } from '@/api/api.generatedTypes' +import { Pagination, usePagination } from '@pagopa/interop-fe-commons' +import React from 'react' +import { useTranslation } from 'react-i18next' +import PlusOneIcon from '@mui/icons-material/PlusOne' +import { Stack } from '@mui/material' +import { keepPreviousData, useQuery } from '@tanstack/react-query' +import { AuthHooks } from '@/api/auth' +import { DelegationQueries } from '@/api/delegation' +import { Link } from '@/router' +import { DelegationsTable } from '@/components/shared/DelegationTable/DelegationsTable' + +export const DelegationsGrantedTab: React.FC = () => { + const { t: tCommon } = useTranslation('common') + const { isAdmin } = AuthHooks.useJwt() + const currentUserOrganizationId = AuthHooks.useJwt().jwt?.organizationId + + const { paginationParams, paginationProps, getTotalPageCount } = usePagination({ limit: 10 }) + + const defaultParams: Pick = { + delegatorIds: [currentUserOrganizationId as string], + } + + const queryParams = { + ...paginationParams, + ...defaultParams, + } + + const { data: totalPageCount = 0 } = useQuery({ + ...DelegationQueries.getProducerDelegationsList(queryParams), + placeholderData: keepPreviousData, + select: ({ pagination }) => getTotalPageCount(pagination.totalCount), + }) + + return ( + <> + {isAdmin && ( + + } + > + {tCommon('createNewBtn')} + + + )} + + + + ) +} diff --git a/src/pages/DelegationsPage/components/DelegationsReceivedTab/DelegationsReceivedTab.tsx b/src/pages/DelegationsPage/components/DelegationsReceivedTab/DelegationsReceivedTab.tsx new file mode 100644 index 000000000..57454e376 --- /dev/null +++ b/src/pages/DelegationsPage/components/DelegationsReceivedTab/DelegationsReceivedTab.tsx @@ -0,0 +1,40 @@ +import type { GetDelegationsParams } from '@/api/api.generatedTypes' +import { AuthHooks } from '@/api/auth' +import { Pagination, usePagination } from '@pagopa/interop-fe-commons' +import React from 'react' +import { + DelegationsTable, + DelegationsTableSkeleton, +} from '../../../../components/shared/DelegationTable/DelegationsTable' +import { keepPreviousData, useQuery } from '@tanstack/react-query' +import { DelegationQueries } from '@/api/delegation' +import type { DelegationType } from '@/types/party.types' + +export const DelegationsReceivedTab: React.FC = () => { + const { jwt } = AuthHooks.useJwt() + const { paginationParams, paginationProps, getTotalPageCount } = usePagination({ limit: 10 }) + + const params: GetDelegationsParams = { + ...paginationParams, + kind: 'DELEGATED_PRODUCER', + delegateIds: [jwt?.organizationId as string], + } + + const { data: totalPageCount = 0 } = useQuery({ + ...DelegationQueries.getProducerDelegationsList(params), + placeholderData: keepPreviousData, + enabled: Boolean(jwt?.organizationId), + select: ({ pagination }) => getTotalPageCount(pagination.totalCount), + }) + + return ( + <> + } + > + + + + + ) +} diff --git a/src/pages/DelegationsPage/index.ts b/src/pages/DelegationsPage/index.ts new file mode 100644 index 000000000..f15e33978 --- /dev/null +++ b/src/pages/DelegationsPage/index.ts @@ -0,0 +1 @@ +export { DelegationsPage } from './Delegations.page' diff --git a/src/pages/ProviderAgreementDetailsPage/components/ProviderAgreementDetailsAttributesSectionsList/ProviderAgreementDetailsVerifiedAttributesSection/ProviderAgreementDetailsVerifiedAttributesDrawer.tsx b/src/pages/ProviderAgreementDetailsPage/components/ProviderAgreementDetailsAttributesSectionsList/ProviderAgreementDetailsVerifiedAttributesSection/ProviderAgreementDetailsVerifiedAttributesDrawer.tsx index 2e7c6c7ee..83a8dbcbe 100644 --- a/src/pages/ProviderAgreementDetailsPage/components/ProviderAgreementDetailsAttributesSectionsList/ProviderAgreementDetailsVerifiedAttributesSection/ProviderAgreementDetailsVerifiedAttributesDrawer.tsx +++ b/src/pages/ProviderAgreementDetailsPage/components/ProviderAgreementDetailsAttributesSectionsList/ProviderAgreementDetailsVerifiedAttributesSection/ProviderAgreementDetailsVerifiedAttributesDrawer.tsx @@ -176,6 +176,7 @@ function useGetDrawerComponents( partyId: agreement.consumer.id, id: attributeId, expirationDate: selectedExpirationDate, + agreementId: agreement.id, }, { onSuccess: closeProviderAgreementVerifiedAttributesDrawer } ) diff --git a/src/pages/ProviderAgreementsListPage/components/ProviderAgreementsTableRow.tsx b/src/pages/ProviderAgreementsListPage/components/ProviderAgreementsTableRow.tsx index 55f60ef5d..569a7a27f 100644 --- a/src/pages/ProviderAgreementsListPage/components/ProviderAgreementsTableRow.tsx +++ b/src/pages/ProviderAgreementsListPage/components/ProviderAgreementsTableRow.tsx @@ -6,7 +6,7 @@ import { ButtonSkeleton } from '@/components/shared/MUI-skeletons' import { StatusChip, StatusChipSkeleton } from '@/components/shared/StatusChip' import useGetAgreementsActions from '@/hooks/useGetAgreementsActions' import { Link } from '@/router' -import { Box, Skeleton } from '@mui/material' +import { Box, Chip, Skeleton } from '@mui/material' import { TableRow } from '@pagopa/interop-fe-commons' import { useQueryClient } from '@tanstack/react-query' import React from 'react' @@ -27,14 +27,26 @@ export const ProviderAgreementsTableRow: React.FC<{ agreement: AgreementListEntr const eservice = agreement.eservice const descriptor = agreement.descriptor + const isDelegatedEservice = eservice.producer.id !== AuthHooks.useJwt().jwt?.organizationId + const handlePrefetch = () => { queryClient.prefetchQuery(AgreementQueries.getSingle(agreement.id)) } + const eserviceCellData = ( + <> + {t('eserviceName', { + name: eservice.name, + version: descriptor.version, + })}{' '} + {isDelegatedEservice && } + + ) + return ( , ]} diff --git a/src/pages/ProviderEServiceDetailsPage/ProviderEServiceDetails.page.tsx b/src/pages/ProviderEServiceDetailsPage/ProviderEServiceDetails.page.tsx index 72958cece..97cdd19d2 100644 --- a/src/pages/ProviderEServiceDetailsPage/ProviderEServiceDetails.page.tsx +++ b/src/pages/ProviderEServiceDetailsPage/ProviderEServiceDetails.page.tsx @@ -22,9 +22,10 @@ const ProviderEServiceDetailsPage: React.FC = () => { ) const { actions } = useGetProviderEServiceActions( - descriptor?.eservice.id, + eserviceId, descriptor?.state, - descriptor?.id, + descriptor?.eservice.draftDescriptor?.state, + descriptorId, descriptor?.eservice.draftDescriptor?.id, descriptor?.eservice.mode ) @@ -34,7 +35,14 @@ const ProviderEServiceDetailsPage: React.FC = () => { title={descriptor?.eservice.name || ''} topSideActions={actions} isLoading={!descriptor} - statusChip={descriptor ? { for: 'eservice', state: descriptor?.state } : undefined} + statusChip={ + descriptor + ? { + for: 'eservice', + state: descriptor?.state, + } + : undefined + } backToAction={{ label: t('actions.backToListLabel'), to: 'PROVIDE_ESERVICE_LIST', diff --git a/src/pages/ProviderEServiceListPage/components/EServiceTableRow.tsx b/src/pages/ProviderEServiceListPage/components/EServiceTableRow.tsx index 94f596779..c73e50893 100644 --- a/src/pages/ProviderEServiceListPage/components/EServiceTableRow.tsx +++ b/src/pages/ProviderEServiceListPage/components/EServiceTableRow.tsx @@ -1,6 +1,6 @@ import React from 'react' import { StatusChip, StatusChipSkeleton } from '@/components/shared/StatusChip' -import { Box, Skeleton, Stack } from '@mui/material' +import { Box, Skeleton, Stack, Typography } from '@mui/material' import { useTranslation } from 'react-i18next' import { Link } from '@/router' import { ActionMenu, ActionMenuSkeleton } from '@/components/shared/ActionMenu' @@ -11,6 +11,8 @@ import { TableRow } from '@pagopa/interop-fe-commons' import type { ProducerEService } from '@/api/api.generatedTypes' import { AuthHooks } from '@/api/auth' import { useQueryClient } from '@tanstack/react-query' +import { ByDelegationChip } from '@/components/shared/ByDelegationChip' +import { useGetDelegationUserRole } from '@/hooks/useGetDelegationUserRole' type EServiceTableRow = { eservice: ProducerEService @@ -18,13 +20,19 @@ type EServiceTableRow = { export const EServiceTableRow: React.FC = ({ eservice }) => { const { t } = useTranslation('common') - const { isAdmin, isOperatorAPI } = AuthHooks.useJwt() + const { isAdmin, isOperatorAPI, jwt } = AuthHooks.useJwt() const queryClient = useQueryClient() + const { isDelegate, isDelegator } = useGetDelegationUserRole({ + eserviceId: eservice.id, + organizationId: jwt?.organizationId, + }) + const { actions } = useGetProviderEServiceActions( eservice.id, eservice.activeDescriptor?.state, + eservice.draftDescriptor?.state, eservice.activeDescriptor?.id, eservice.draftDescriptor?.id, eservice.mode @@ -33,6 +41,8 @@ export const EServiceTableRow: React.FC = ({ eservice }) => { const isEServiceInDraft = !eservice.activeDescriptor const isEServiceEditable = (isAdmin || isOperatorAPI) && isEServiceInDraft + const isEServiceByDelegation = isDelegate || isDelegator + const handlePrefetch = () => { if (isEServiceEditable) { queryClient.prefetchQuery(EServiceQueries.getSingle(eservice.id)) @@ -47,14 +57,25 @@ export const EServiceTableRow: React.FC = ({ eservice }) => { return ( + {eservice.name} + + + ) : ( + eservice.name + ), eservice?.activeDescriptor?.version || '1', {eservice?.activeDescriptor && ( )} {(isEServiceInDraft || eservice?.draftDescriptor) && ( - + )} , ]} diff --git a/src/pages/ProviderEServiceSummaryPage/ProviderEServiceSummary.page.tsx b/src/pages/ProviderEServiceSummaryPage/ProviderEServiceSummary.page.tsx index 29d8a84d5..5f0793637 100644 --- a/src/pages/ProviderEServiceSummaryPage/ProviderEServiceSummary.page.tsx +++ b/src/pages/ProviderEServiceSummaryPage/ProviderEServiceSummary.page.tsx @@ -1,9 +1,9 @@ import React from 'react' import { PageContainer } from '@/components/layout/containers' -import { useTranslation } from 'react-i18next' +import { Trans, useTranslation } from 'react-i18next' import { useNavigate, useParams } from '@/router' import { EServiceMutations, EServiceQueries } from '@/api/eservice' -import { Button, Stack, Tooltip } from '@mui/material' +import { Alert, Button, Link, 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' @@ -16,17 +16,31 @@ import { import { ProviderEServiceAttributeVersionSummary } from './components/ProviderEServiceAttributeVersionSummary' import { ProviderEServiceRiskAnalysisSummaryList } from './components/ProviderEServiceRiskAnalysisSummaryList' import { useQuery } from '@tanstack/react-query' +import { RejectReasonDrawer } from '@/components/shared/RejectReasonDrawer' +import { useDrawerState } from '@/hooks/useDrawerState' +import { AuthHooks } from '@/api/auth' +import { useGetDelegationUserRole } from '@/hooks/useGetDelegationUserRole' const ProviderEServiceSummaryPage: React.FC = () => { const { t } = useTranslation('eservice') const { t: tCommon } = useTranslation('common', { keyPrefix: 'actions' }) + const { jwt } = AuthHooks.useJwt() const { eserviceId, descriptorId } = useParams<'PROVIDE_ESERVICE_SUMMARY'>() const navigate = useNavigate() + const { isOpen, openDrawer, closeDrawer } = useDrawerState() + + const { isDelegator, isDelegate, producerDelegations } = useGetDelegationUserRole({ + eserviceId, + organizationId: jwt?.organizationId, + }) + const { mutate: deleteVersion } = EServiceMutations.useDeleteVersionDraft() const { mutate: deleteDraft } = EServiceMutations.useDeleteDraft() - const { mutate: publishVersion } = EServiceMutations.usePublishVersionDraft() + const { mutate: publishVersion } = EServiceMutations.usePublishVersionDraft({ + isByDelegation: isDelegate, + }) const { data: descriptor, isLoading } = useQuery( EServiceQueries.getDescriptorProvider(eserviceId, descriptorId) @@ -63,8 +77,18 @@ const ProviderEServiceSummaryPage: React.FC = () => { const handlePublishDraft = () => { if (!descriptor) return + + const delegation = producerDelegations?.find( + (delegation) => delegation.eservice?.id === eserviceId + ) + publishVersion( - { eserviceId: descriptor.eservice.id, descriptorId: descriptor.id }, + { + eserviceId: descriptor.eservice.id, + descriptorId: descriptor.id, + delegatorName: delegation?.delegator.name, + eserviceName: delegation?.eservice?.name, + }, { onSuccess: () => navigate('PROVIDE_ESERVICE_MANAGE', { @@ -91,6 +115,12 @@ const ProviderEServiceSummaryPage: React.FC = () => { const isReceiveMode = descriptor?.eservice.mode === 'RECEIVE' + const sortedRejectedReasons = descriptor?.rejectionReasons?.slice().sort((a, b) => { + const dateA = new Date(a.rejectedAt) + const dateB = new Date(b.rejectedAt) + return dateB.getTime() - dateA.getTime() + }) + return ( { to: 'PROVIDE_ESERVICE_LIST', }} isLoading={isLoading} - statusChip={{ for: 'eservice', state: 'DRAFT' }} + statusChip={{ + for: 'eservice', + state: 'DRAFT', + isDraftToCorrect: descriptor && descriptor.rejectionReasons?.length !== 0, + }} > + {descriptor && descriptor.rejectionReasons?.length !== 0 && ( + + + ), + }} + > + {isDelegator + ? t('summary.rejectedDelegatedVersionDraftAlert.delegator') + : t('summary.rejectedDelegatedVersionDraftAlert.delegate')} + + + )} + }> @@ -146,20 +201,30 @@ const ProviderEServiceSummaryPage: React.FC = () => { - - - - - + {isDelegate && ( + + + + + + )} + + {sortedRejectedReasons && sortedRejectedReasons.length !== 0 && ( + + )} ) } diff --git a/src/pages/TenantCertifierPage/components/AssignAttributesTab/AssignAttributeDrawer.tsx b/src/pages/TenantCertifierPage/components/AssignAttributesTab/AssignAttributeDrawer.tsx index 3b2be9b57..e84c5bf9f 100644 --- a/src/pages/TenantCertifierPage/components/AssignAttributesTab/AssignAttributeDrawer.tsx +++ b/src/pages/TenantCertifierPage/components/AssignAttributesTab/AssignAttributeDrawer.tsx @@ -1,4 +1,4 @@ -import type { CompactAttribute, CompactTenant } from '@/api/api.generatedTypes' +import type { CompactAttribute, CompactTenant, TenantFeature } from '@/api/api.generatedTypes' import { AttributeMutations, AttributeQueries } from '@/api/attribute' import { TenantHooks, TenantQueries } from '@/api/tenant' import { Drawer } from '@/components/shared/Drawer' @@ -56,12 +56,16 @@ export const AssignAttributeDrawer: React.FC = ({ } const { data: activeTenant } = TenantHooks.useGetActiveUserParty() + const certifierId = activeTenant.features.find( + (feature): feature is Extract => + Boolean('certifier' in feature && feature.certifier?.certifierId) + )?.certifier?.certifierId const { data: attributeOptions = [] } = useQuery({ ...AttributeQueries.getList({ limit: 50, offset: 0, kinds: ['CERTIFIED'], - origin: activeTenant.features[0]?.certifier?.certifierId, + origin: certifierId, q: getAttributeQ(), }), placeholderData: keepPreviousData, diff --git a/src/pages/TenantCertifierPage/components/ManageAttributesTab/ManageAttributesTab.tsx b/src/pages/TenantCertifierPage/components/ManageAttributesTab/ManageAttributesTab.tsx index cb6dc5d6c..e815caa82 100644 --- a/src/pages/TenantCertifierPage/components/ManageAttributesTab/ManageAttributesTab.tsx +++ b/src/pages/TenantCertifierPage/components/ManageAttributesTab/ManageAttributesTab.tsx @@ -1,4 +1,4 @@ -import type { GetAttributesParams } from '@/api/api.generatedTypes' +import type { GetAttributesParams, TenantFeature } from '@/api/api.generatedTypes' import { AttributeQueries } from '@/api/attribute' import { TenantHooks } from '@/api/tenant' import { useDrawerState } from '@/hooks/useDrawerState' @@ -24,8 +24,12 @@ export const ManageAttributesTab: React.FC = () => { >([{ name: 'q', label: t('filters.nameField.label'), type: 'freetext' }]) const { data: activeParty } = TenantHooks.useGetActiveUserParty() + const certifierId = activeParty.features.find( + (feature): feature is Extract => + Boolean('certifier' in feature && feature.certifier?.certifierId) + )?.certifier?.certifierId const defaultParams: Pick = { - origin: activeParty?.features[0]?.certifier?.certifierId, + origin: certifierId, kinds: ['CERTIFIED'], } diff --git a/src/pages/index.ts b/src/pages/index.ts index 4a5b43a50..ff5f70ae8 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -38,3 +38,6 @@ export { ProviderKeychainDetailsPage } from './ProviderKeychainDetailsPage' export { ProviderKeychainCreatePage } from './ProviderKeychainCreatePage' export { ProviderKeychainPublicKeyDetailsPage } from './ProviderKeychainPublicKeyDetailsPage' export { ProviderKeychainUserDetailsPage } from './ProviderKeychainUserDetailsPage' +export { DelegationsPage } from './DelegationsPage' +export { DelegationCreatePage } from './DelegationCreatePage' +export { DelegationDetailsPage } from './DelegationDetailsPage' diff --git a/src/router/components/RoutesWrapper/AuthGuard.tsx b/src/router/components/RoutesWrapper/AuthGuard.tsx index ce5cffef3..0c84cfa16 100644 --- a/src/router/components/RoutesWrapper/AuthGuard.tsx +++ b/src/router/components/RoutesWrapper/AuthGuard.tsx @@ -4,6 +4,7 @@ import type { RouteKey } from '@/router' import { useAuthGuard, useCurrentRoute } from '@/router' import type { JwtUser, UserProductRole } from '@/types/party.types' import { ForbiddenError } from '@/utils/errors.utils' +import { isTenantCertifier } from '@/utils/tenant.utils' import { useQuery } from '@tanstack/react-query' import React from 'react' @@ -39,7 +40,7 @@ export const AuthGuard: React.FC = ({ const isInBlacklist = jwt?.organizationId && blacklist?.includes(jwt.organizationId) function isUserAllowedToAccessCertifierRoutes() { - const isCertifier = Boolean(tenant?.features[0]?.certifier?.certifierId) + const isCertifier = isTenantCertifier(tenant) const certifierRoutes: Array = [ 'TENANT_CERTIFIER', 'TENANT_CERTIFIER_ATTRIBUTE_DETAILS', diff --git a/src/router/routes.tsx b/src/router/routes.tsx index 78e98beed..4383661f4 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -41,6 +41,9 @@ import { ProviderKeychainDetailsPage, ProviderKeychainUserDetailsPage, ProviderKeychainPublicKeyDetailsPage, + DelegationsPage, + DelegationCreatePage, + DelegationDetailsPage, } from '@/pages' import RoutesWrapper from './components/RoutesWrapper' import type { LangCode } from '@/types/common.types' @@ -437,6 +440,30 @@ export const { routes, reactRouterDOMRoutes, hooks, components, utils } = new In hideSideNav: false, authLevels: ['admin', 'support', 'security'], }) + .addRoute({ + key: 'DELEGATIONS', + path: '/aderente/deleghe', + element: , + public: false, + hideSideNav: false, + authLevels: ['admin', 'support'], + }) + .addRoute({ + key: 'CREATE_DELEGATION', + path: '/aderente/deleghe/crea', + element: , + public: false, + hideSideNav: true, + authLevels: ['admin', 'support'], + }) + .addRoute({ + key: 'DELEGATION_DETAILS', + path: '/aderente/deleghe/:delegationId', + element: , + public: false, + hideSideNav: false, + authLevels: ['admin', 'support'], + }) .build() export type RouteKey = InferRouteKey diff --git a/src/static/locales/en/agreement.json b/src/static/locales/en/agreement.json index 84f1737f1..d0e95b320 100644 --- a/src/static/locales/en/agreement.json +++ b/src/static/locales/en/agreement.json @@ -227,6 +227,7 @@ }, "list": { "eserviceName": "{{name}}, version {{version}}", + "eserviceChip": "in delega", "filters": { "eserviceField": { "label": "Find by e-service" diff --git a/src/static/locales/en/common.json b/src/static/locales/en/common.json index 78ff979b9..0e0718c5e 100644 --- a/src/static/locales/en/common.json +++ b/src/static/locales/en/common.json @@ -39,7 +39,9 @@ "revoke": "Revoke", "upload": "Upload", "import": "Import", - "saveEdits": "Save edits" + "saveEdits": "Save edits", + "accept": "Accept", + "approve": "Approve" }, "table": { "headData": { @@ -59,7 +61,10 @@ "certifiedAttributes": "Certified attributes", "assigneeTenant": "Assignee tenant", "certifiedAttribute": "Certified attribute", - "keychain": "Keychain" + "keychain": "Keychain", + "delegationKind": "Delegation for", + "delegatorName": "Delegation requested by", + "delegateName": "Delegation granted to" } }, "status": { @@ -68,7 +73,9 @@ "DRAFT": "draft", "SUSPENDED": "suspended", "ARCHIVED": "archived", - "DEPRECATED": "deprecated" + "DEPRECATED": "deprecated", + "WAITING_FOR_APPROVAL": "waiting for approval", + "DRAFT_TO_CORRECT": "to correct" }, "agreement": { "ACTIVE": "active", @@ -91,6 +98,12 @@ "WAITING_FOR_APPROVAL": "waiting for approval", "ARCHIVED": "archived", "REJECTED": "rejected" + }, + "delegation": { + "ACTIVE": "active", + "REJECTED": "rejected", + "REVOKED": "revoked", + "WAITING_FOR_APPROVAL": "waiting for acceptance" } }, "userProductRole": { diff --git a/src/static/locales/en/eservice.json b/src/static/locales/en/eservice.json index 8a9126d5f..657044214 100644 --- a/src/static/locales/en/eservice.json +++ b/src/static/locales/en/eservice.json @@ -211,6 +211,10 @@ }, "notPublishableTooltip": { "label": "Uno o più campi richiesti non sono stati compilati. Completa la bozza" + }, + "rejectedDelegatedVersionDraftAlert": { + "delegate": "The publication of this draft was rejected by the delegator. <1>Read the reasons for rejection and resend the draft with the corrections.", + "delegator": "You have reject the publication of this draft. <1>Read the reasons for rejection here." } }, "list": { diff --git a/src/static/locales/en/mutations-feedback.json b/src/static/locales/en/mutations-feedback.json index f521ce53a..3e4976efb 100644 --- a/src/static/locales/en/mutations-feedback.json +++ b/src/static/locales/en/mutations-feedback.json @@ -86,6 +86,20 @@ "description": "You will not be able to delete an e-service version after it is published. It will also become available in the e-service catalog. It will always be possible to suspend it or make it deprecated when a new version is published" } }, + "publishDelegatedVersionDraft": { + "loading": "We are forwarding the version to the delegator", + "outcome": { + "success": "New e-service version was forwarded to the delegator to approve its publication", + "error": "It was not possible to forward the new e-service version to the delegator for approval. Please try again!" + }, + "confirmDialog": { + "title": "Request new eservice version publication", + "description": "You are about to forward to {{delegatorName}} the request to publish a new version of the {{eserviceName}} e-service.", + "actions": { + "proceed": "Submit request" + } + } + }, "addEServiceRiskAnalysis": { "loading": "Adding the risk analysis to this e-service", "outcome": { @@ -191,6 +205,20 @@ "success": "The {{attributeKind}} attributes have been modified correctly", "error": "It was not possible to modify the {{attributeKind}} attributes. Please, try again!" } + }, + "approveDelegatedVersionDraft": { + "loading": "We are publishing the version", + "outcome": { + "success": "The new version of e-service has been published successfully", + "error": "It was not possible to publish the new version of e-service. Please try again!" + } + }, + "rejectDelegatedVersionDraft": { + "loading": "We are rejecting the version", + "outcome": { + "success": "The e-service version has returned to draft, and the reasons for the refusal have been notified to the delivery delegate", + "error": "It was not possible to reject the new version of e-service. Please try again!" + } } }, "attribute": { @@ -578,6 +606,13 @@ "success": "Contact info updated successfully", "error": "Contact Infos could not be updated. Please, try again!" } + }, + "updateProducerDelegationAvailability": { + "loading": "Updating delegation availability", + "outcome": { + "success": "Delegation availability updated succesfully", + "error": "Delegation availability could not be updated. Please, try again!" + } } }, "voucher": { @@ -656,5 +691,41 @@ "error": "The user could not be removed from the keychain. Please try again!" } } + }, + "delegation": { + "createProducerDelegation": { + "loading": "We are forwarding the delegation", + "outcome": { + "success": "The new delegation has been created correctly and has been forwarded to the member for evaluation", + "error": "The delegation could not be forwarded. Please try again!" + } + }, + "approveProducerDelegation": { + "loading": "We are activating the delegation", + "outcome": { + "success": "Delegation has been activated successfully. From this moment you can manage the provision of the e-service by delegation", + "error": "Delegation could not be activated. Please try again!" + } + }, + "rejectProducerDelegation": { + "loading": "We are rejecting the delegation", + "outcome": { + "success": "The delegation was successfully rejected.", + "error": "It was not possible to reject the delegation. Please try again!" + } + }, + "revokeProducerDelegation": { + "loading": "We are revoking the delegation", + "outcome": { + "success": "The delegation was successfully revoked.", + "error": "It was not possible to revoke the delegation. Please try again!" + } + }, + "downloadDelegationContract": { + "loading": "Downloading contract", + "outcome": { + "error": "It was not possible to download the contract. Please try again" + } + } } } diff --git a/src/static/locales/en/pages.json b/src/static/locales/en/pages.json index 7ce1e056c..f6d0301cb 100644 --- a/src/static/locales/en/pages.json +++ b/src/static/locales/en/pages.json @@ -42,5 +42,9 @@ "providerKeychainsList": { "title": "Your keychains", "description": "Here you can manage you keychains" + }, + "delegations": { + "title": "Delegations", + "description": "Here you can manage all the delegations you have given or received from other entities" } } diff --git a/src/static/locales/en/party.json b/src/static/locales/en/party.json index b96b7d337..269993e63 100644 --- a/src/static/locales/en/party.json +++ b/src/static/locales/en/party.json @@ -100,5 +100,120 @@ }, "backToTenantCertifierBtn": "Return to tenant certifier" } + }, + "delegations": { + "tabs": { + "ariaLabel": "Three different tabs for managing granted delegations, received delegations and the availability to be delegated", + "delegationsGranted": "Delegations granted", + "delegationsReceived": "Delegations received", + "availability": "Availability" + }, + "list": { + "noDelegationsLabel": "There are no delegations to show", + "delegationKind": { + "producer": "Producer", + "consumer": "Consumer" + } + }, + "create": { + "title": "Create delegation", + "kindSectionTitle": "Delegation's kinds", + "consumeDelegationTitle": "Consume delegation", + "provideDelegationTitle": "Provide delegation", + "cancelBtn": "Cancel", + "backWithoutSaveBtn": "Go back", + "forwardWithSaveBtn": "Next", + "submitBtn": "Submit delegation", + "cards": { + "common": "I want to create a ", + "consume": "consumer's delegation", + "provide": "provider's delegation" + }, + "eserviceField": { + "label": "E-service name (required)", + "infoLabel": "Min 5 chars, max 60 chars", + "infoLabelAutocomplete": "The list includes only those e-services for which the respective producers have given availability to consume by delegation.", + "descriptionLabel": "E-service description", + "descriptionInfoLabel": "Include input and output data in the description. Min 10 chars, max 250 chars" + }, + "delegateField": { + "consume": { + "label": "Delegated tenant (required)", + "infoLabel": "The list includes only members who have given their willingness to become consumer delegates." + }, + "provide": { + "label": "Delegated tenant (required)", + "infoLabel": "The list includes only members who have given their willingness to become provider delegates.", + "switch": "I have already created the e-service for which I want to activate a delegation" + } + }, + "dialog": { + "title": "Appointment of data controller", + "description": "Please note that is the obligation of the delegator to proceed at the appointment of the delegate as the person responsible for the processing of personal data by formalizing the act provided for in art. 28 of GDPR, before to star any processing activity through the infrastructure.", + "proceedLabel": "Submit delegation", + "cancelLabel": "Cancella", + "checkboxLabel": "I understand" + } + }, + "actions": { + "backToTenant": "Return to tenant", + "backToDelegations": "Return to delegations" + }, + "details": { + "title": "Manage delegation", + "backToDelegationList": "Back to delegations", + "generalInfoSection": { + "title": "General Information", + "eserviceNameField": { + "label": "E-Service name" + }, + "eserviceProducerField": { + "label": "Producer" + }, + "delegationKindField": { + "label": "Kind", + "kindProducer": "Producer delegation", + "kindConsumer": "Consumer delegation" + }, + "delegatorField": { + "label": "Delegator" + }, + "delegateField": { + "label": "Delegate" + }, + "submissionDateField": { + "label": "Submission date" + }, + "delegationStateField": { + "label": "Delegation state" + }, + "downloadContractAction": { + "label": "Download delegation document" + }, + "downloadRevokedContractAction": { + "label": "Download revoked delegation document" + } + }, + "rejectedDelegationAlert": "This delegation has been rejected. <1>Read here the reject reason." + }, + "availabilityTab": { + "title": "Availability to accept new delegations", + "consumeDelegation": { + "label": "Consume delegation", + "infoLabel": "Availability of tenant to consume e-service on behalf of others tenants", + "value": { + "true": "Yes, I am available", + "false": "No, I am not available" + } + }, + "produceDelegation": { + "label": "Provide delegation", + "infoLabel": "Availability of tenant to provide e-service on behalf of others tenants", + "value": { + "true": "Yes, I am available", + "false": "No, I am not available" + } + } + } } } diff --git a/src/static/locales/en/shared-components.json b/src/static/locales/en/shared-components.json index 83b597a45..b99f7539a 100644 --- a/src/static/locales/en/shared-components.json +++ b/src/static/locales/en/shared-components.json @@ -143,6 +143,52 @@ "title": "Confirm public key deletion", "description": "The key can no longer be used to sign responses to users of your e-services. To find out more, <1>read the guide." }, + "dialogAcceptProducerDelegation": { + "title": "Appointment of Data Controller", + "content": { + "description": "Please remember that it is necessary to have been appointed as Personal Data Controller, by signing the deed required by art. 28 of the GDPR, before starting any processing activity via the infrastructure on behalf of the Delegator, Data Controller.", + "confirmationLabel": "I understand" + }, + "actions": { + "accept": "Accept delegation" + } + }, + "dialogRejectProducerDelegation": { + "title": "Reject delegation", + "content": { + "reason": { + "label": "Reason", + "infoLabel": "Enter the reason why you refuse the delegation. Min 10 characters, max 250 characters" + } + }, + "actions": { + "reject": "Reject delegation" + } + }, + "dialogRevokeProducerDelegation": { + "title": "Revocation of the producer delegation", + "content": { + "description": "The delegate will lose management of your e-service {{eserviceName}} with producer delegation, and the agreements and the purposes associated with it. The e-service will return under your control in the state it was in at the time of revocation. Do you have doubts? <1>Read the guide.", + "checkbox": "I have read and I want to proceed with the revocation" + } + }, + "dialogRejectDelegatedVersionDraft": { + "title": "Reject publication", + "description": "The reason for your refusal will be reported to your delegate, who will be able to update the draft and send it back to you for approval for publication.", + "content": { + "reason": { + "label": "Reason", + "infoLabel": "Enter the reason why you refuse publication. Min 10 characters, max 250 characters" + } + } + }, + "dialogApproveDelegatedVersionDraft": { + "title": "Publish version", + "description": "You are about to publish a new version of your e-service {{eserviceName}} with delegation of delivery developed by your delegate {{delegateName}}. At the same time, you approve the risk analyzes compiled by the delegate acting on your behalf.", + "actions": { + "approveAndPublish": "Approve and publish" + } + }, "copyToClipboardButton": { "copy": "Copy", "copied": "Copied" @@ -207,7 +253,11 @@ "removeGroupAriaLabel": "Remove attribute group" }, "drawer": { - "closeIconAriaLabel": "Close the drawer" + "closeIconAriaLabel": "Close the drawer", + "updateLabel": "Update" + }, + "byDelegationChip": { + "label": "by delegation" }, "riskAnalysis": { "formComponents": { @@ -281,6 +331,9 @@ "PROVIDE_KEYCHAIN_CREATE": "Create keychain", "PROVIDE_KEYCHAIN_DETAILS": "Manage keychain", "PROVIDE_KEYCHAIN_USER_DETAILS": "Manage keychain user", - "PROVIDE_KEYCHAIN_PUBLIC_KEY_DETAILS": "Manage keychain public key" + "PROVIDE_KEYCHAIN_PUBLIC_KEY_DETAILS": "Manage keychain public key", + "DELEGATIONS": "Delegations", + "CREATE_DELEGATION": "Create delegation", + "DELEGATION_DETAILS": "Manage delegation" } } diff --git a/src/static/locales/it/agreement.json b/src/static/locales/it/agreement.json index 8444a8abf..863869ead 100644 --- a/src/static/locales/it/agreement.json +++ b/src/static/locales/it/agreement.json @@ -227,6 +227,7 @@ }, "list": { "eserviceName": "{{name}}, v. {{version}}", + "eserviceChip": "in delega", "filters": { "eserviceField": { "label": "Cerca per e-service" diff --git a/src/static/locales/it/common.json b/src/static/locales/it/common.json index e739ee2a8..cd8227876 100644 --- a/src/static/locales/it/common.json +++ b/src/static/locales/it/common.json @@ -39,7 +39,9 @@ "revoke": "Revoca", "upload": "Carica", "import": "Importa", - "saveEdits": "Salva modifiche" + "saveEdits": "Salva modifiche", + "accept": "Accetta", + "approve": "Approva" }, "table": { "headData": { @@ -58,8 +60,11 @@ "userName": "Nome e cognome", "certifiedAttributes": "Attributi certificati", "assigneeTenant": "Ente assegnatario", + "keychain": "Portachiavi", "certifiedAttribute": "Attributo certificato", - "keychain": "Portachiavi" + "delegationKind": "Delega per", + "delegatorName": "Delega richiesta da", + "delegateName": "Delega conferita a" } }, "status": { @@ -68,7 +73,9 @@ "DRAFT": "in bozza", "SUSPENDED": "sospeso", "ARCHIVED": "archiviato", - "DEPRECATED": "deprecato" + "DEPRECATED": "deprecato", + "WAITING_FOR_APPROVAL": "in attesa di approvazione", + "DRAFT_TO_CORRECT": "da correggere" }, "agreement": { "ACTIVE": "attivo", @@ -91,6 +98,12 @@ "WAITING_FOR_APPROVAL": "in attesa di approvazione", "ARCHIVED": "archiviata", "REJECTED": "rifiutata" + }, + "delegation": { + "ACTIVE": "attiva", + "REJECTED": "rifiutata", + "REVOKED": "revocata", + "WAITING_FOR_APPROVAL": "in attesa di accettazione" } }, "userProductRole": { diff --git a/src/static/locales/it/eservice.json b/src/static/locales/it/eservice.json index ebaa49ab0..103192ae1 100644 --- a/src/static/locales/it/eservice.json +++ b/src/static/locales/it/eservice.json @@ -211,6 +211,10 @@ }, "notPublishableTooltip": { "label": "Uno o più campi richiesti non sono stati compilati. Completa la bozza" + }, + "rejectedDelegatedVersionDraftAlert": { + "delegate": "La pubblicazione di questa bozza è stata rifiutata dal delegante. <1>Leggi i motivi del rifiuto ed invia di nuovo la bozza con le correzioni.", + "delegator": "Hai rifiutato la pubblicazione di questa bozza. <1>Leggi qui i motivi del rifiuto." } }, "list": { diff --git a/src/static/locales/it/mutations-feedback.json b/src/static/locales/it/mutations-feedback.json index 73327944b..c7af67e3d 100644 --- a/src/static/locales/it/mutations-feedback.json +++ b/src/static/locales/it/mutations-feedback.json @@ -86,6 +86,20 @@ "description": "Una volta pubblicata, una versione dell'e-service non è più cancellabile e diventa disponibile nel catalogo degli e-service. Sarà comunque possibile sospenderla, o renderla obsoleta una volta che una nuova versione diventa disponibile." } }, + "publishDelegatedVersionDraft": { + "loading": "Stiamo inoltrando la versione al delegante", + "outcome": { + "success": "La versione è stata inoltrata al delegante per approvarne la pubblicazione", + "error": "Non è stato possibile inoltrare al delegante la versione per approvazione. Per favore, riprova!" + }, + "confirmDialog": { + "title": "Richiedi pubblicazione versione", + "description": "Stai per inoltrare a {{delegatorName}} la richiesta di pubblicazione di una nuova versione dell’e-service {{eserviceName}}.", + "actions": { + "proceed": "Inoltra richiesta" + } + } + }, "addEServiceRiskAnalysis": { "loading": "Stiamo salvando la finalità", "outcome": { @@ -191,6 +205,20 @@ "success": "Gli attributi {{attributeKind}} sono stati modificati correttamente!", "error": "Non è stato possibile modificare gli attributi {{attributeKind}}. Per favore, riprova!" } + }, + "approveDelegatedVersionDraft": { + "loading": "Stiamo pubblicando la versione", + "outcome": { + "success": "La nuova versione di e-service è stata pubblicata correttamente", + "error": "Non è stato possibile pubblicare la nuova versione di e-service. Per favore, riprova!" + } + }, + "rejectDelegatedVersionDraft": { + "loading": "Stiamo rifiutando la versione", + "outcome": { + "success": "La versione di e-service è tornata in bozza, e le motivazioni del rifiuto sono state notificate al delegato all’erogazione", + "error": "Non è stato possibile rifiutare la nuova versione di e-service. Per favore, riprova!" + } } }, "attribute": { @@ -578,6 +606,13 @@ "success": "I contatti sono stati aggiornati correttamente", "error": "Non è stato possibile aggiornare i contatti. Per favore, riprova!" } + }, + "updateProducerDelegationAvailability": { + "loading": "Stiamo aggiornando la disponibilità", + "outcome": { + "success": "La disponibilità è stata aggiornata correttamente", + "error": "Non è stato possibile aggiornare la disponibilità. Per favore, riprova!" + } } }, "voucher": { @@ -656,5 +691,41 @@ "error": "Non è stato possibile rimuovere l'utente dal portachiavi. Per favore, riprova!" } } + }, + "delegation": { + "createProducerDelegation": { + "loading": "Stiamo inoltrando la delega", + "outcome": { + "success": "La nuova delega è stata creata correttamente ed è stata inoltrata all’aderente per valutazione", + "error": "Non è stato possibile inoltrare la delega. Per favore, riprova!" + } + }, + "approveProducerDelegation": { + "loading": "Stiamo attivando la delega", + "outcome": { + "success": "La delega è stata attivata correttamente. Da questo momento puoi gestire l’erogazione dell’e-service in delega", + "error": "Non è stato possibile attivare la delega. Per favore, riprova!" + } + }, + "rejectProducerDelegation": { + "loading": "Stiamo rifiutando la delega", + "outcome": { + "success": "La delega è stata rifiutata correttamente.", + "error": "Non è stato possibile rifiutare la delega. Per favore, riprova!" + } + }, + "revokeProducerDelegation": { + "loading": "Stiamo revocando la delega", + "outcome": { + "success": "La delega è stata revocata correttamente.", + "error": "Non è stato possibile revocare la delega. Per favore, riprova!" + } + }, + "downloadDelegationContract": { + "loading": "Stiamo scaricando il contratto", + "outcome": { + "error": "Non è stato possibile scaricare il contratto. Per favore, riprova!" + } + } } } diff --git a/src/static/locales/it/pages.json b/src/static/locales/it/pages.json index ed254dccf..ced2354cc 100644 --- a/src/static/locales/it/pages.json +++ b/src/static/locales/it/pages.json @@ -42,5 +42,9 @@ "providerKeychainsList": { "title": "I tuoi portachiavi", "description": "Qui puoi gestire i tuoi portachiavi" + }, + "delegations": { + "title": "Deleghe", + "description": "Qui puoi gestire tutte le deleghe che hai dato o ricveuto da altri enti" } } diff --git a/src/static/locales/it/party.json b/src/static/locales/it/party.json index 57ce07a2e..35e1331fe 100644 --- a/src/static/locales/it/party.json +++ b/src/static/locales/it/party.json @@ -100,5 +100,120 @@ }, "backToTenantCertifierBtn": "Torna a ente certificatore" } + }, + "delegations": { + "tabs": { + "ariaLabel": "Tre tab diverse per la gestione delle deleghe conferite, ricevute e per la disponibilità ad essere delegati", + "delegationsGranted": "Deleghe conferite", + "delegationsReceived": "Deleghe ricevute", + "availability": "Disponibilità" + }, + "list": { + "noDelegationsLabel": "Non ci sono deleghe da mostrare", + "delegationKind": { + "producer": "Erogazione", + "consumer": "Fruizione" + } + }, + "create": { + "title": "Crea delega", + "kindSectionTitle": "Tipologie di delega", + "consumeDelegationTitle": "Delega alla fruizione", + "provideDelegationTitle": "Delega all'erogazione", + "cancelBtn": "Annulla", + "backWithoutSaveBtn": "Torna indietro", + "forwardWithSaveBtn": "Prosegui", + "submitBtn": "Inoltra delega", + "cards": { + "common": "Voglio creare una ", + "consume": "delega alla fruizione", + "provide": "delega all’erogazione" + }, + "eserviceField": { + "label": "Nome dell'e-service (richiesto)", + "infoLabel": "Min 5 caratteri, max 60 caratteri", + "infoLabelAutocomplete": "La lista include solo gli e-service per i quali i rispettivi erogatori hanno dato disponibilità all’erogazione per delega.", + "descriptionLabel": "Descrizione dell'e-service", + "descriptionInfoLabel": "Includere nel testo i dati di input e output. Min 10 caratteri, max 250 caratteri" + }, + "delegateField": { + "consume": { + "label": "Ente da delegare alla fruizione (richiesto)", + "infoLabel": "La lista include solo gli aderenti che hanno dato la loro disponibilità a diventare delegati alla fruizione" + }, + "provide": { + "label": "Ente da delegare all'erogazione (richiesto)", + "infoLabel": "La lista include solo gli aderenti che hanno dato la loro disponibilità a diventare delegati all’erogazione", + "switch": "Ho già creato l’e-service per il quale voglio attivare una delega" + } + }, + "dialog": { + "title": "Nomina Responsabile del Trattamento", + "description": "Si ricorda che è obbligo del delegante procedere alla nomina del delegato quale Responsabile del Trattamento dei dati personali, formalizzando l'atto previsto dall'art. 28 del GDPR, prima di avviare qualsiasi attività di trattamento tramite l'infrastruttura.", + "proceedLabel": "Inoltra delega", + "cancelLabel": "Annulla", + "checkboxLabel": "Ho capito" + } + }, + "actions": { + "backToTenant": "Torna al tuo ente", + "backToDelegations": "Torna alle deleghe" + }, + "details": { + "title": "Gestisci delega", + "backToDelegationList": "Torna a deleghe", + "generalInfoSection": { + "title": "Informazioni generali", + "eserviceNameField": { + "label": "Nome e-service" + }, + "eserviceProducerField": { + "label": "Erogatore" + }, + "delegationKindField": { + "label": "Tipologia", + "kindProducer": "Delega all’erogazione", + "kindConsumer": "Delega alla fruizione" + }, + "delegatorField": { + "label": "Delegante" + }, + "delegateField": { + "label": "Delegato" + }, + "submissionDateField": { + "label": "Data di richiesta" + }, + "delegationStateField": { + "label": "Stato della delega" + }, + "downloadContractAction": { + "label": "Scarica il documento di delega" + }, + "downloadRevokedContractAction": { + "label": "Scarica il documento di revoca della delega" + } + }, + "rejectedDelegationAlert": "Questa delega è stata rifiutata. <1>Leggi qui i motivi del rifiuto." + }, + "availabilityTab": { + "title": "Disponibilità ad accettare nuove deleghe", + "consumeDelegation": { + "label": "Delega alla fruizione", + "infoLabel": "La disponibilità dell’ente a fruire di e-service per conto di altri enti", + "value": { + "true": "Sì, sono disponibile", + "false": "No, non sono disponibile" + } + }, + "produceDelegation": { + "label": "Delega all’erogazione", + "infoLabel": "La disponibilità dell’ente a erogare e-service per conto di altri enti", + "value": { + "true": "Sì, sono disponibile", + "false": "No, non sono disponibile" + } + } + } } } diff --git a/src/static/locales/it/shared-components.json b/src/static/locales/it/shared-components.json index d16b8a66d..f6ec27e9f 100644 --- a/src/static/locales/it/shared-components.json +++ b/src/static/locales/it/shared-components.json @@ -143,6 +143,52 @@ "title": "Conferma eliminazione chiave pubblica", "description": "La chiave non potrà più essere utilizzata per firmare risposte verso i fruitori dei tuoi e-service. Per saperne di più, <1>leggi la guida." }, + "dialogAcceptProducerDelegation": { + "title": "Nomina Responsabile del Trattamento", + "content": { + "description": "Si ricorda che è necessario essere stati nominati Responsabile del Trattamento ai dati personali, con sottoscrizione dell'atto previsto dall'art. 28 del GDPR, prima di avviare qualsiasi attività di trattamento tramite l'infrastruttura per conto del Delegante, Titolare del Trattamento.", + "confirmationLabel": "Ho capito" + }, + "actions": { + "accept": "Accetta delega" + } + }, + "dialogRejectProducerDelegation": { + "title": "Rifiuta delega", + "content": { + "reason": { + "label": "Motivazione", + "infoLabel": "Inserisci la motivazione per la quale rifiuti la delega. Min 10 caratteri, max 250 caratteri" + } + }, + "actions": { + "reject": "Rifiuta delega" + } + }, + "dialogRevokeProducerDelegation": { + "title": "Revoca della delega all'erogazione", + "content": { + "description": "Il delegato perderà la gestione del tuo e-service {{eserviceName}} con delega all'erogazione, e delle richieste di fruizione e le finalità ad esso associate. L’e-service tornerà sotto il tuo controllo nello stato in cui si trova all’atto della revoca. Hai dubbi? <1>Consulta la guida.", + "checkbox": "Ho letto e voglio procedere con la revoca" + } + }, + "dialogRejectDelegatedVersionDraft": { + "title": "Rifiuta pubblicazione", + "description": "La motivazione del tuo rifiuto sarà riportata al tuo delegato, il quale potrà aggiornare la bozza e inviartela nuovamente in approvazione per la pubblicazione.", + "content": { + "reason": { + "label": "Motivazione", + "infoLabel": "Inserisci la motivazione per la quale rifiuti la pubblicazione. Min 10 caratteri, max 250 caratteri" + } + } + }, + "dialogApproveDelegatedVersionDraft": { + "title": "Pubblica versione", + "description": "Stai per pubblicare una nuova versione del tuo e-service {{eserviceName}} con delega all'erogazione messa a punto dal tuo delegato {{delegateName}}. Contestualmente, approvi le analisi del rischio compilate dal delegato che opera per tuo conto.", + "actions": { + "approveAndPublish": "Approva e pubblica" + } + }, "copyToClipboardButton": { "copy": "Copia", "copied": "Copiato" @@ -207,7 +253,11 @@ "removeGroupAriaLabel": "Rimuovi gruppo di attributi" }, "drawer": { - "closeIconAriaLabel": "Chiudi il drawer" + "closeIconAriaLabel": "Chiudi il drawer", + "updateLabel": "Aggiorna" + }, + "byDelegationChip": { + "label": "in delega" }, "riskAnalysis": { "formComponents": { @@ -281,6 +331,9 @@ "PROVIDE_KEYCHAIN_CREATE": "Crea portachiavi", "PROVIDE_KEYCHAIN_DETAILS": "Gestisci portachiavi", "PROVIDE_KEYCHAIN_USER_DETAILS": "Gestisci utente del portachiavi", - "PROVIDE_KEYCHAIN_PUBLIC_KEY_DETAILS": "Gestisci chiave pubblica del portachiavi" + "PROVIDE_KEYCHAIN_PUBLIC_KEY_DETAILS": "Gestisci chiave pubblica del portachiavi", + "DELEGATIONS": "Deleghe", + "CREATE_DELEGATION": "Crea delega", + "DELEGATION_DETAILS": "Gestisci delega" } } diff --git a/src/types/dialog.types.ts b/src/types/dialog.types.ts index 0c92dedee..abb52f0d8 100644 --- a/src/types/dialog.types.ts +++ b/src/types/dialog.types.ts @@ -28,6 +28,11 @@ export type DialogProps = | DialogSetTenantMailProps | DialogRemoveUserFromKeychainProps | DialogDeleteProducerKeychainKeyProps + | DialogDelegationsProps + | DialogAcceptProducerDelegationProps + | DialogRejectProducerDelegationProps + | DialogRevokeProducerDelegationProps + | DialogRejectDelegatedVersionDraftProps export type DialogAttributeDetailsProps = { type: 'showAttributeDetails' @@ -104,3 +109,30 @@ export type DialogDeleteProducerKeychainKeyProps = { keychainId: string keyId: string } + +export type DialogDelegationsProps = { + type: 'delegations' + onConfirm: () => void +} + +export type DialogAcceptProducerDelegationProps = { + type: 'acceptDelegation' + delegationId: string +} + +export type DialogRejectProducerDelegationProps = { + type: 'rejectDelegation' + delegationId: string +} + +export type DialogRevokeProducerDelegationProps = { + type: 'revokeProducerDelegation' + delegationId: string + eserviceName: string +} + +export type DialogRejectDelegatedVersionDraftProps = { + type: 'rejectDelegatedVersionDraft' + eserviceId: string + descriptorId: string +} diff --git a/src/types/party.types.ts b/src/types/party.types.ts index 03037b0a6..7d671fb03 100644 --- a/src/types/party.types.ts +++ b/src/types/party.types.ts @@ -2,6 +2,8 @@ import type { ExternalId } from '@/api/api.generatedTypes' export type UserProductRole = 'admin' | 'security' | 'api' | 'support' +export type DelegationType = 'DELEGATION_GRANTED' | 'DELEGATION_RECEIVED' + type JwtOrg = { name: string roles: Array<{ diff --git a/src/utils/__tests__/eservice.utils.test.ts b/src/utils/__tests__/eservice.utils.test.ts index e2890874d..6d660e1ca 100644 --- a/src/utils/__tests__/eservice.utils.test.ts +++ b/src/utils/__tests__/eservice.utils.test.ts @@ -1,4 +1,4 @@ -import { getDownloadDocumentName } from '../eservice.utils' +import { getDownloadDocumentName, getLastDescriptor } from '../eservice.utils' describe('getDownloadDocumentName utility function testing', () => { it('should correctly get the document namy from a DocumentRead data type', () => { @@ -12,3 +12,15 @@ describe('getDownloadDocumentName utility function testing', () => { expect(result).toEqual('document.pdf') }) }) + +describe('getLastDescriptor utility function testing', () => { + it('should correctly get the last descriptor from an array of descriptors', () => { + const result = getLastDescriptor([ + { id: 'test-id-1', state: 'PUBLISHED', version: '1', audience: ['test-audience'] }, + { id: 'test-id-2', state: 'PUBLISHED', version: '2', audience: ['test-audience'] }, + { id: 'test-id-3', state: 'PUBLISHED', version: '3', audience: ['test-audience'] }, + ]) + + expect(result?.id).toEqual('test-id-3') + }) +}) diff --git a/src/utils/__tests__/tenant.utils.test.ts b/src/utils/__tests__/tenant.utils.test.ts new file mode 100644 index 000000000..d4dad337a --- /dev/null +++ b/src/utils/__tests__/tenant.utils.test.ts @@ -0,0 +1,30 @@ +import { TenantFeature } from '@/api/api.generatedTypes' +import { hasTenantGivenProducerDelegationAvailability, isTenantCertifier } from '../tenant.utils' + +const mockTenant = { + id: 'test-id', + name: 'test-name', + externalId: { origin: 'test-origin', value: 'test-value' }, + features: [ + { + certifier: { certifierId: 'test-certifierId' }, + delegatedProducer: { availabilityTimestamp: 'test-timestamp' }, + }, + ], + createdAt: 'test-createdAt', + attributes: { declared: [], verified: [], certified: [] }, +} + +describe('isTenantCertifier utility function testing', () => { + it('should correctly verify if tenant is certifier', () => { + const result = isTenantCertifier(mockTenant) + expect(result).toBe(true) + }) +}) + +describe('hasTenantGivenProducerDelegationAvailability utility function testing', () => { + it('should correctly verify if tenant has given the producer delegations availability', () => { + const result = hasTenantGivenProducerDelegationAvailability(mockTenant) + expect(result).toEqual('test-timestamp') + }) +}) diff --git a/src/utils/eservice.utils.ts b/src/utils/eservice.utils.ts index bf735a179..0fe95057f 100644 --- a/src/utils/eservice.utils.ts +++ b/src/utils/eservice.utils.ts @@ -1,4 +1,4 @@ -import type { EServiceDoc, Document } from '@/api/api.generatedTypes' +import type { EServiceDoc, Document, CompactDescriptor } from '@/api/api.generatedTypes' export function getDownloadDocumentName(document: EServiceDoc | Document) { const filename: string = document.name @@ -6,3 +6,10 @@ export function getDownloadDocumentName(document: EServiceDoc | Document) { const fileExtension = filenameBits[filenameBits.length - 1] return `${document.prettyName}.${fileExtension}` } + +export function getLastDescriptor(descriptors: Array) { + const descriptor = descriptors.find((descriptor) => + descriptors.every((d) => descriptor.version >= d.version) + ) + return descriptor +} diff --git a/src/utils/tenant.utils.ts b/src/utils/tenant.utils.ts new file mode 100644 index 000000000..8e25ee0c1 --- /dev/null +++ b/src/utils/tenant.utils.ts @@ -0,0 +1,12 @@ +import { Tenant, TenantFeature } from '@/api/api.generatedTypes' + +export function isTenantCertifier(tenant: Tenant) { + return tenant.features.some((feature) => 'certifier' in feature && feature.certifier?.certifierId) +} + +export function hasTenantGivenProducerDelegationAvailability(tenant: Tenant) { + return tenant.features.find( + (feature): feature is Extract => + Boolean('delegatedProducer' in feature && feature.delegatedProducer?.availabilityTimestamp) + )?.delegatedProducer?.availabilityTimestamp +}