diff --git a/backend/src/api/documents/document-template.controller.ts b/backend/src/api/documents/document-template.controller.ts index a9376b6e..7ac089e8 100644 --- a/backend/src/api/documents/document-template.controller.ts +++ b/backend/src/api/documents/document-template.controller.ts @@ -44,14 +44,17 @@ export class DocumentTemplateController { @Post() async create( @Body() payload: CreateDocumentTemplateDto, - @ExtractUser() { organizationId, id: adminId }: IAdminUserModel, + @ExtractUser() admin: IAdminUserModel, ): Promise { const newDocumentTemplate = - await this.createDocumentTemplateUsecase.execute({ - ...payload, - createdByAdminId: adminId, - organizationId, - }); + await this.createDocumentTemplateUsecase.execute( + { + ...payload, + createdByAdminId: admin.id, + organizationId: admin.organizationId, + }, + admin, + ); return new DocumentTemplatePresenter(newDocumentTemplate); } @@ -74,11 +77,11 @@ export class DocumentTemplateController { async update( @Param('id', UuidValidationPipe) id: string, @Body() payload: CreateDocumentTemplateDto, - @ExtractUser() { organizationId }: IAdminUserModel, + @ExtractUser() admin: IAdminUserModel, ): Promise { return this.updateDocumentTemplateUsecase.execute( { id, ...payload }, - organizationId, + admin, ); } @@ -86,9 +89,9 @@ export class DocumentTemplateController { @Delete(':id') async delete( @Param('id', UuidValidationPipe) id: string, - @ExtractUser() { organizationId }: IAdminUserModel, + @ExtractUser() admin: IAdminUserModel, ): Promise { - return this.deleteDocumentTemplateUsecase.execute(id, organizationId); + return this.deleteDocumentTemplateUsecase.execute(id, admin); } @Get() diff --git a/backend/src/instrument.ts b/backend/src/instrument.ts index abec5659..85ec7892 100644 --- a/backend/src/instrument.ts +++ b/backend/src/instrument.ts @@ -13,7 +13,7 @@ Sentry.init({ debug: process.env.NODE_ENV === 'local', - enabled: true, + enabled: process.env.NODE_ENV !== 'local', environment: process.env.NODE_ENV, diff --git a/backend/src/migrations/1727684897827-AddTemplateActionsArchive.ts b/backend/src/migrations/1727684897827-AddTemplateActionsArchive.ts new file mode 100644 index 00000000..2fd5ec1e --- /dev/null +++ b/backend/src/migrations/1727684897827-AddTemplateActionsArchive.ts @@ -0,0 +1,35 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class Migrations1727684897827 implements MigrationInterface { + name = 'Migrations1727684897827'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TYPE "public"."actions_archive_event_name_enum" RENAME TO "actions_archive_event_name_enum_old"`, + ); + await queryRunner.query( + `CREATE TYPE "public"."actions_archive_event_name_enum" AS ENUM('UPDATE_ORGANIZATION_PROFILE', 'CREATE_ORGANIZATION_STRUCTURE', 'UPDATE_ORGANIZATION_STRUCTURE', 'DELETE_ORGANIZATION_STRUCTURE', 'APPROVE_ACCESS_REQUEST', 'REJECT_ACCESS_REQUEST', 'DELETE_ACCESS_REQUEST', 'CHANGE_VOLUNTEER_STATUS', 'UPDATE_VOLUNTEER_PROFILE', 'REGISTER_ACTIVITY_LOG', 'CHANGE_ACTIVITY_LOG_STATUS', 'CREATE_ACTIVITY_TYPE', 'UPDATE_ACTIVITY_TYPE', 'CHANGE_ACTIVITY_TYPE_STATUS', 'CREATE_EVENT', 'UPDATE_EVENT', 'DELETE_EVENT', 'CHANGE_EVENT_STATUS', 'CREATE_ANNOUNCEMENT', 'DELETE_ANNOUNCEMENT', 'PUBLISH_ANNOUNCEMENT', 'CREATE_CONTRACT', 'APPROVE_CONTRACT', 'REJECT_CONTRACT', 'CREATE_DOCUMENT_CONTRACT', 'VALIDATE_DOCUMENT_CONTRACT', 'SIGN_DOCUMENT_CONTRACT_BY_NGO', 'SIGN_DOCUMENT_CONTRACT_BY_VOLUNTEER', 'REJECT_DOCUMENT_CONTRACT_BY_NGO', 'REJECT_DOCUMENT_CONTRACT_BY_VOLUNTEER', 'DELETE_DOCUMENT_CONTRACT', 'CREATE_DOCUMENT_TEMPLATE', 'UPDATE_DOCUMENT_TEMPLATE', 'DELETE_DOCUMENT_TEMPLATE')`, + ); + await queryRunner.query( + `ALTER TABLE "actions_archive" ALTER COLUMN "event_name" TYPE "public"."actions_archive_event_name_enum" USING "event_name"::"text"::"public"."actions_archive_event_name_enum"`, + ); + await queryRunner.query( + `DROP TYPE "public"."actions_archive_event_name_enum_old"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "public"."actions_archive_event_name_enum_old" AS ENUM('UPDATE_ORGANIZATION_PROFILE', 'CREATE_ORGANIZATION_STRUCTURE', 'UPDATE_ORGANIZATION_STRUCTURE', 'DELETE_ORGANIZATION_STRUCTURE', 'APPROVE_ACCESS_REQUEST', 'REJECT_ACCESS_REQUEST', 'DELETE_ACCESS_REQUEST', 'CHANGE_VOLUNTEER_STATUS', 'UPDATE_VOLUNTEER_PROFILE', 'REGISTER_ACTIVITY_LOG', 'CHANGE_ACTIVITY_LOG_STATUS', 'CREATE_ACTIVITY_TYPE', 'UPDATE_ACTIVITY_TYPE', 'CHANGE_ACTIVITY_TYPE_STATUS', 'CREATE_EVENT', 'UPDATE_EVENT', 'DELETE_EVENT', 'CHANGE_EVENT_STATUS', 'CREATE_ANNOUNCEMENT', 'DELETE_ANNOUNCEMENT', 'PUBLISH_ANNOUNCEMENT', 'CREATE_CONTRACT', 'APPROVE_CONTRACT', 'REJECT_CONTRACT', 'CREATE_DOCUMENT_CONTRACT', 'VALIDATE_DOCUMENT_CONTRACT', 'SIGN_DOCUMENT_CONTRACT_BY_NGO', 'SIGN_DOCUMENT_CONTRACT_BY_VOLUNTEER', 'REJECT_DOCUMENT_CONTRACT_BY_NGO', 'REJECT_DOCUMENT_CONTRACT_BY_VOLUNTEER', 'DELETE_DOCUMENT_CONTRACT')`, + ); + await queryRunner.query( + `ALTER TABLE "actions_archive" ALTER COLUMN "event_name" TYPE "public"."actions_archive_event_name_enum_old" USING "event_name"::"text"::"public"."actions_archive_event_name_enum_old"`, + ); + await queryRunner.query( + `DROP TYPE "public"."actions_archive_event_name_enum"`, + ); + await queryRunner.query( + `ALTER TYPE "public"."actions_archive_event_name_enum_old" RENAME TO "actions_archive_event_name_enum"`, + ); + } +} diff --git a/backend/src/modules/actions-archive/enums/action-resource-types.enum.ts b/backend/src/modules/actions-archive/enums/action-resource-types.enum.ts index 6e867fb8..d1daa555 100644 --- a/backend/src/modules/actions-archive/enums/action-resource-types.enum.ts +++ b/backend/src/modules/actions-archive/enums/action-resource-types.enum.ts @@ -5,7 +5,10 @@ import { ContractStatus } from 'src/modules/documents/enums/contract-status.enum import { EventStatus } from 'src/modules/event/enums/event-status.enum'; import { OrganizationStructureType } from 'src/modules/organization/enums/organization-structure-type.enum'; import { VolunteerStatus } from 'src/modules/volunteer/enums/volunteer-status.enum'; -import { BaseDocumentContractActionsArchiveEvent } from '../interfaces/document-contract-actions-archive-event.type'; +import { + BaseDocumentContractActionsArchiveEvent, + BaseDocumentTemplateActionsArchiveEvent, +} from '../interfaces/document-contract-actions-archive-event.type'; export enum TrackedEventName { // Organization Profile @@ -60,9 +63,9 @@ export enum TrackedEventName { DELETE_DOCUMENT_CONTRACT = 'DELETE_DOCUMENT_CONTRACT', // New Templates - // CREATE_DOCUMENT_TEMPLATE = 'CREATE_DOCUMENT_TEMPLATE', - // UPDATE_DOCUMENT_TEMPLATE = 'UPDATE_DOCUMENT_TEMPLATE', - // DELETE_DOCUMENT_TEMPLATE = 'DELETE_DOCUMENT_TEMPLATE', + CREATE_DOCUMENT_TEMPLATE = 'CREATE_DOCUMENT_TEMPLATE', + UPDATE_DOCUMENT_TEMPLATE = 'UPDATE_DOCUMENT_TEMPLATE', + DELETE_DOCUMENT_TEMPLATE = 'DELETE_DOCUMENT_TEMPLATE', } export interface TrackedEventData { @@ -230,4 +233,9 @@ export interface TrackedEventData { rejectionReason: string; }; [TrackedEventName.DELETE_DOCUMENT_CONTRACT]: BaseDocumentContractActionsArchiveEvent; + + // New Templates + [TrackedEventName.CREATE_DOCUMENT_TEMPLATE]: BaseDocumentTemplateActionsArchiveEvent; + [TrackedEventName.UPDATE_DOCUMENT_TEMPLATE]: BaseDocumentTemplateActionsArchiveEvent; + [TrackedEventName.DELETE_DOCUMENT_TEMPLATE]: BaseDocumentTemplateActionsArchiveEvent; } diff --git a/backend/src/modules/actions-archive/interfaces/document-contract-actions-archive-event.type.ts b/backend/src/modules/actions-archive/interfaces/document-contract-actions-archive-event.type.ts index fd6e1b7f..5c4ba6d6 100644 --- a/backend/src/modules/actions-archive/interfaces/document-contract-actions-archive-event.type.ts +++ b/backend/src/modules/actions-archive/interfaces/document-contract-actions-archive-event.type.ts @@ -6,3 +6,9 @@ export type BaseDocumentContractActionsArchiveEvent = { volunteerId: string; volunteerName: string; }; + +export type BaseDocumentTemplateActionsArchiveEvent = { + organizationId: string; + documentTemplateId: string; + documentTemplateName: string; +}; diff --git a/backend/src/modules/documents/repositories/document-template.repository.ts b/backend/src/modules/documents/repositories/document-template.repository.ts index 68132558..89feafd0 100644 --- a/backend/src/modules/documents/repositories/document-template.repository.ts +++ b/backend/src/modules/documents/repositories/document-template.repository.ts @@ -63,12 +63,14 @@ export class DocumentTemplateRepositoryService return DocumentTemplateTransformer.fromEntity(documentTemplate); } - async delete(options: DeleteOneDocumentTemplateOptions): Promise { + async delete( + options: DeleteOneDocumentTemplateOptions, + ): Promise<{ name: string }> { const template = await this.documentTemplateRepository.findOneBy(options); if (template) { await this.documentTemplateRepository.remove(template); - return options.id; + return { name: template.name }; } return null; diff --git a/backend/src/modules/documents/services/document-template.facade.ts b/backend/src/modules/documents/services/document-template.facade.ts index a5478281..e52e9f1c 100644 --- a/backend/src/modules/documents/services/document-template.facade.ts +++ b/backend/src/modules/documents/services/document-template.facade.ts @@ -49,7 +49,9 @@ export class DocumentTemplateFacade { return this.documentTemplateListViewRepository.findMany(findOptions); } - async delete(options: DeleteOneDocumentTemplateOptions): Promise { + async delete( + options: DeleteOneDocumentTemplateOptions, + ): Promise<{ name: string }> { return this.documentTemplateRepository.delete(options); } } diff --git a/backend/src/usecases/documents/new_contracts/create-document-contract.usecase.ts b/backend/src/usecases/documents/new_contracts/create-document-contract.usecase.ts index 8017a589..b1c5fdad 100644 --- a/backend/src/usecases/documents/new_contracts/create-document-contract.usecase.ts +++ b/backend/src/usecases/documents/new_contracts/create-document-contract.usecase.ts @@ -144,7 +144,6 @@ export class CreateDocumentContractUsecase implements IUseCaseService { // 8. Generate the PDF try { - // TODO: Make it async, so we can return the contract id immediately await this.documentPDFGenerator.generateContractPDF(contract.id); } catch (error) { this.logger.error( diff --git a/backend/src/usecases/documents/new_contracts/create-document-template.usecase.ts b/backend/src/usecases/documents/new_contracts/create-document-template.usecase.ts index f82a5763..8164e874 100644 --- a/backend/src/usecases/documents/new_contracts/create-document-template.usecase.ts +++ b/backend/src/usecases/documents/new_contracts/create-document-template.usecase.ts @@ -2,12 +2,15 @@ import { Injectable, Logger } from '@nestjs/common'; import { JSONStringifyError } from 'src/common/helpers/utils'; import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface'; import { ExceptionsService } from 'src/infrastructure/exceptions/exceptions.service'; +import { ActionsArchiveFacade } from 'src/modules/actions-archive/actions-archive.facade'; +import { TrackedEventName } from 'src/modules/actions-archive/enums/action-resource-types.enum'; import { DocumentTemplateExceptionMessages } from 'src/modules/documents/exceptions/documente-template.exceptions'; import { CreateDocumentTemplateOptions, IDocumentTemplateModel, } from 'src/modules/documents/models/document-template.model'; import { DocumentTemplateFacade } from 'src/modules/documents/services/document-template.facade'; +import { IAdminUserModel } from 'src/modules/user/models/admin-user.model'; @Injectable() export class CreateDocumentTemplateUsecase @@ -17,14 +20,26 @@ export class CreateDocumentTemplateUsecase constructor( private readonly documentTemplateFacade: DocumentTemplateFacade, private readonly exceptionService: ExceptionsService, + private readonly actionsArchiveFacade: ActionsArchiveFacade, ) {} public async execute( options: CreateDocumentTemplateOptions, + admin: IAdminUserModel, ): Promise { try { const newTemplate = await this.documentTemplateFacade.create(options); + this.actionsArchiveFacade.trackEvent( + TrackedEventName.CREATE_DOCUMENT_TEMPLATE, + { + organizationId: newTemplate.organizationId, + documentTemplateId: newTemplate.id, + documentTemplateName: newTemplate.name, + }, + admin, + ); + return newTemplate; } catch (error) { this.logger.error({ diff --git a/backend/src/usecases/documents/new_contracts/delete-document-template.usecase.ts b/backend/src/usecases/documents/new_contracts/delete-document-template.usecase.ts index 13799b4d..28e86e7e 100644 --- a/backend/src/usecases/documents/new_contracts/delete-document-template.usecase.ts +++ b/backend/src/usecases/documents/new_contracts/delete-document-template.usecase.ts @@ -2,9 +2,12 @@ import { Injectable, Logger } from '@nestjs/common'; import { JSONStringifyError } from 'src/common/helpers/utils'; import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface'; import { ExceptionsService } from 'src/infrastructure/exceptions/exceptions.service'; +import { ActionsArchiveFacade } from 'src/modules/actions-archive/actions-archive.facade'; +import { TrackedEventName } from 'src/modules/actions-archive/enums/action-resource-types.enum'; import { DocumentTemplateExceptionMessages } from 'src/modules/documents/exceptions/documente-template.exceptions'; import { DocumentContractFacade } from 'src/modules/documents/services/document-contract.facade'; import { DocumentTemplateFacade } from 'src/modules/documents/services/document-template.facade'; +import { IAdminUserModel } from 'src/modules/user/models/admin-user.model'; @Injectable() export class DeleteDocumentTemplateUsecase implements IUseCaseService { @@ -13,14 +16,15 @@ export class DeleteDocumentTemplateUsecase implements IUseCaseService { private readonly documentTemplateFacade: DocumentTemplateFacade, private readonly documentContractFacade: DocumentContractFacade, private readonly exceptionService: ExceptionsService, + private readonly actionsArchiveFacade: ActionsArchiveFacade, ) {} - public async execute(id: string, organizationId: string): Promise { + public async execute(id: string, admin: IAdminUserModel): Promise { try { // 1. Templates can be deleted if are not linked with a contract const isUsed = await this.documentContractFacade.exists({ documentTemplateId: id, - organizationId: organizationId, + organizationId: admin.organizationId, }); if (isUsed) { @@ -32,7 +36,7 @@ export class DeleteDocumentTemplateUsecase implements IUseCaseService { // 2. Try to delete it const deleted = await this.documentTemplateFacade.delete({ id, - organizationId, + organizationId: admin.organizationId, }); if (!deleted) { @@ -41,7 +45,17 @@ export class DeleteDocumentTemplateUsecase implements IUseCaseService { ); } - return deleted; + this.actionsArchiveFacade.trackEvent( + TrackedEventName.DELETE_DOCUMENT_TEMPLATE, + { + organizationId: admin.organizationId, + documentTemplateId: id, + documentTemplateName: deleted.name, + }, + admin, + ); + + return deleted.name; } catch (error) { if (error?.status === 400) { // Rethrow errors that we've thrown above, and catch the others diff --git a/backend/src/usecases/documents/new_contracts/update-document-template.usecase.ts b/backend/src/usecases/documents/new_contracts/update-document-template.usecase.ts index ae5d06e6..42193fa1 100644 --- a/backend/src/usecases/documents/new_contracts/update-document-template.usecase.ts +++ b/backend/src/usecases/documents/new_contracts/update-document-template.usecase.ts @@ -1,11 +1,15 @@ import { Injectable, Logger } from '@nestjs/common'; +import { ObjectDiff } from 'src/common/helpers/object-diff'; import { JSONStringifyError } from 'src/common/helpers/utils'; import { IUseCaseService } from 'src/common/interfaces/use-case-service.interface'; import { ExceptionsService } from 'src/infrastructure/exceptions/exceptions.service'; +import { ActionsArchiveFacade } from 'src/modules/actions-archive/actions-archive.facade'; +import { TrackedEventName } from 'src/modules/actions-archive/enums/action-resource-types.enum'; import { DocumentTemplateExceptionMessages } from 'src/modules/documents/exceptions/documente-template.exceptions'; import { UpdateDocumentTemplateOptions } from 'src/modules/documents/models/document-template.model'; import { DocumentContractFacade } from 'src/modules/documents/services/document-contract.facade'; import { DocumentTemplateFacade } from 'src/modules/documents/services/document-template.facade'; +import { IAdminUserModel } from 'src/modules/user/models/admin-user.model'; @Injectable() export class UpdateDocumentTemplateUsecase implements IUseCaseService { @@ -14,17 +18,18 @@ export class UpdateDocumentTemplateUsecase implements IUseCaseService { private readonly documentTemplateFacade: DocumentTemplateFacade, private readonly documentContractFacade: DocumentContractFacade, private readonly exceptionService: ExceptionsService, + private readonly actionsArchiveFacade: ActionsArchiveFacade, ) {} public async execute( updates: UpdateDocumentTemplateOptions, - organizationId: string, + admin: IAdminUserModel, ): Promise { try { // 1. Does the template exists in the callers' organization? - const template = await this.documentTemplateFacade.exists({ + const template = await this.documentTemplateFacade.findOne({ id: updates.id, - organizationId, + organizationId: admin.organizationId, }); if (!template) { @@ -36,7 +41,7 @@ export class UpdateDocumentTemplateUsecase implements IUseCaseService { // 2. Templates can be deleted if are not linked with a contract const isUsed = await this.documentContractFacade.exists({ documentTemplateId: updates.id, - organizationId: organizationId, + organizationId: admin.organizationId, }); if (isUsed) { @@ -45,7 +50,18 @@ export class UpdateDocumentTemplateUsecase implements IUseCaseService { ); } - await this.documentTemplateFacade.update(updates); + const updated = await this.documentTemplateFacade.update(updates); + + this.actionsArchiveFacade.trackEvent( + TrackedEventName.UPDATE_DOCUMENT_TEMPLATE, + { + organizationId: admin.organizationId, + documentTemplateId: updates.id, + documentTemplateName: template.name, + }, + admin, + ObjectDiff.diff(template, updated), + ); } catch (error) { if (error?.status === 400) { // Rethrow errors that we've thrown above, and catch the others diff --git a/frontend/src/assets/locales/en/translation.json b/frontend/src/assets/locales/en/translation.json index 431c4865..aafb5ce7 100644 --- a/frontend/src/assets/locales/en/translation.json +++ b/frontend/src/assets/locales/en/translation.json @@ -806,6 +806,18 @@ "DELETE_DOCUMENT_CONTRACT": { "label": "Delete Contract", "description": "Contract number {{contractNumber}} for {{volunteerName}} has been deleted" + }, + "CREATE_DOCUMENT_TEMPLATE": { + "label": "Create Template", + "description": "A new template has been created with the name {{documentTemplateName}}" + }, + "UPDATE_DOCUMENT_TEMPLATE": { + "label": "Update Template", + "description": "Template with the name {{documentTemplateName}} has been modified." + }, + "DELETE_DOCUMENT_TEMPLATE": { + "label": "Delete Template", + "description": "Template with the name {{documentTemplateName}} has been deleted." } }, "dashboard": { @@ -1300,4 +1312,4 @@ "clear": "Clear", "apply_all": "Apply to all" } -} \ No newline at end of file +} diff --git a/frontend/src/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json index e9bfa8d1..a9cd2ba4 100644 --- a/frontend/src/assets/locales/ro/translation.json +++ b/frontend/src/assets/locales/ro/translation.json @@ -810,6 +810,18 @@ "DELETE_DOCUMENT_CONTRACT": { "label": "Șterge Contract", "description": "Contractul cu numărul {{contractNumber}} pentru {{volunteerName}} a fost șters" + }, + "CREATE_DOCUMENT_TEMPLATE": { + "label": "Creează Template", + "description": "A fost creat un nou template cu numele {{documentTemplateName}}" + }, + "UPDATE_DOCUMENT_TEMPLATE": { + "label": "Actualizează Template", + "description": "Template-ul cu numele {{documentTemplateName}} a fost modificat." + }, + "DELETE_DOCUMENT_TEMPLATE": { + "label": "Șterge Template", + "description": "Template-ul cu numele {{documentTemplateName}} a fost șters." } }, "dashboard": { diff --git a/frontend/src/common/enums/actions.enum.ts b/frontend/src/common/enums/actions.enum.ts index e3fdb2db..c7031ddc 100644 --- a/frontend/src/common/enums/actions.enum.ts +++ b/frontend/src/common/enums/actions.enum.ts @@ -49,4 +49,9 @@ export enum TrackedEventName { REJECT_DOCUMENT_CONTRACT_BY_NGO = 'REJECT_DOCUMENT_CONTRACT_BY_NGO', // done REJECT_DOCUMENT_CONTRACT_BY_VOLUNTEER = 'REJECT_DOCUMENT_CONTRACT_BY_VOLUNTEER', // cannot do it DELETE_DOCUMENT_CONTRACT = 'DELETE_DOCUMENT_CONTRACT', + + // New templates + CREATE_DOCUMENT_TEMPLATE = 'CREATE_DOCUMENT_TEMPLATE', + UPDATE_DOCUMENT_TEMPLATE = 'UPDATE_DOCUMENT_TEMPLATE', + DELETE_DOCUMENT_TEMPLATE = 'DELETE_DOCUMENT_TEMPLATE', } diff --git a/frontend/src/common/interfaces/action.interface.ts b/frontend/src/common/interfaces/action.interface.ts index e0d21b23..c74fc420 100644 --- a/frontend/src/common/interfaces/action.interface.ts +++ b/frontend/src/common/interfaces/action.interface.ts @@ -35,4 +35,6 @@ export interface IEventData { contractNumber?: string; documentContractId?: string; documentContractNumber?: string; + documentTemplateId?: string; + documentTemplateName?: string; } diff --git a/frontend/src/common/utils/actions-archive.mappings.tsx b/frontend/src/common/utils/actions-archive.mappings.tsx index 0b3b8350..ec8c996b 100644 --- a/frontend/src/common/utils/actions-archive.mappings.tsx +++ b/frontend/src/common/utils/actions-archive.mappings.tsx @@ -408,6 +408,22 @@ export const mapEventDataToActionDescription = ( }} /> ); + case TrackedEventName.CREATE_DOCUMENT_TEMPLATE: + case TrackedEventName.UPDATE_DOCUMENT_TEMPLATE: + case TrackedEventName.DELETE_DOCUMENT_TEMPLATE: + return ( + + ), + }} + /> + ); default: return '-'; diff --git a/lambda-pdf-generator/handler.js b/lambda-pdf-generator/handler.js index e014a7d8..1d642563 100644 --- a/lambda-pdf-generator/handler.js +++ b/lambda-pdf-generator/handler.js @@ -33,7 +33,7 @@ exports.generatePDF = async (event) => { const page = await browser.newPage(); await page.setContent(event.body); - const buffer = await page.pdf({ format: 'A4' }); + const buffer = await page.pdf({ format: 'A4', margin: { top: '50px', bottom: '50px' } }); await browser.close(); const fileName = `documents/contracts/${organizationId}/${documentContractId}-${Date.now()}.pdf`; diff --git a/mobile/app.config.ts b/mobile/app.config.ts index 3c4b302a..ea850be2 100644 --- a/mobile/app.config.ts +++ b/mobile/app.config.ts @@ -7,7 +7,7 @@ const expoConfig: ExpoConfig = { name: 'vic', slug: 'vic', scheme: 'vic', - version: '1.1.0', + version: '1.1.1', orientation: 'portrait', icon: './src/assets/images/icon.png', userInterfaceStyle: 'light', @@ -18,7 +18,7 @@ const expoConfig: ExpoConfig = { }, assetBundlePatterns: ['**/*'], ios: { - buildNumber: '24', + buildNumber: '25', supportsTablet: false, bundleIdentifier: 'org.commitglobal.vic', entitlements: { @@ -32,7 +32,7 @@ const expoConfig: ExpoConfig = { }, }, android: { - versionCode: 24, + versionCode: 25, adaptiveIcon: { foregroundImage: './src/assets/images/adaptive-icon.png', backgroundColor: '#ffffff', diff --git a/mobile/package-lock.json b/mobile/package-lock.json index b807c666..43632e97 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -40,7 +40,7 @@ "expo-status-bar": "~1.12.1", "expo-updates": "~0.25.17", "i18next": "^22.4.15", - "lottie-react-native": "6.7.0", + "lottie-react-native": "^6.7.0", "react": "18.2.0", "react-content-loader": "^6.2.1", "react-hook-form": "^7.43.9", diff --git a/mobile/package.json b/mobile/package.json index 8f4e1d0b..26a2b010 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -44,7 +44,7 @@ "expo-status-bar": "~1.12.1", "expo-updates": "~0.25.17", "i18next": "^22.4.15", - "lottie-react-native": "6.7.0", + "lottie-react-native": "^6.7.0", "react": "18.2.0", "react-content-loader": "^6.2.1", "react-hook-form": "^7.43.9", @@ -80,4 +80,4 @@ } }, "private": true -} \ No newline at end of file +} diff --git a/mobile/src/assets/animations/loading-document.json b/mobile/src/assets/animations/loading-document.json new file mode 100644 index 00000000..6393bbeb --- /dev/null +++ b/mobile/src/assets/animations/loading-document.json @@ -0,0 +1 @@ +{"v":"5.5.2","fr":60,"ip":0,"op":275,"w":100,"h":100,"nm":"Comp 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Document line 2","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-136.56,93.086,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[-140.692,153.861,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-89.375,-98.375],[-74.656,-98.375]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.9725,0.8824,0.3216,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.872,0.04,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-89.375,-98.375],[-70.531,-98.375]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.9725,0.8824,0.3216,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.872,0.04,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,8.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":67,"s":[0]},{"t":133,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":67,"s":[0]},{"t":133,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":1255,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Document line 1","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-139.022,80.007,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[-140.692,153.861,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-89.375,-98.375],[-67.375,-98.375]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.9725,0.8824,0.3216,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.872,0.04,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-1.759,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":65,"s":[0]},{"t":132,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":65,"s":[0]},{"t":132,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":1255,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Document outline","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[51.188,47.5,0],"ix":2},"a":{"a":0,"k":[-29.545,-69.237,0],"ix":1},"s":{"a":0,"k":[-71.077,64.994,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[-120.732,-50.774]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.9725,0.8824,0.3216,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.872,0.04,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 4","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"st","c":{"a":0,"k":[0.9725,0.8824,0.3216,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.872,0.04,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 3","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"st","c":{"a":0,"k":[0.9725,0.8824,0.3216,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.872,0.04,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 2","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-59.706,-89.93],[-9.49,-89.984],[1.135,-74.984],[1.135,-5.974],[-59.911,-5.974],[-59.756,-80.466]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.9725,0.8824,0.3216,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[3.354,-17.644],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100.335,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[1]},{"t":135,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":135,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":5,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":1255,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/mobile/src/assets/locales/en/translation.json b/mobile/src/assets/locales/en/translation.json index 97c3f2e6..acb0d819 100644 --- a/mobile/src/assets/locales/en/translation.json +++ b/mobile/src/assets/locales/en/translation.json @@ -805,6 +805,7 @@ "pending_documents": "Pending documents", "approved_history": "Contract history", "expired": "Expired", + "action_expired": "Action expired", "rejected": "Rejected", "current": "Current", "contract": { @@ -815,6 +816,7 @@ "approved": "The contract has been approved by both parties and cannot be modified. Download and view the contract.", "expired": "The contract period has ended. Download and view the contract.", "rejected": "The contract has been rejected.", + "action_expired": "The contract was not signed within the dedicated time frame.", "sign": "Sign", "reject": "Reject contract" }, @@ -865,6 +867,12 @@ "title": "An error occurred while rejecting the contract" } } + }, + "loading_signature": { + "title": "Signing the document...", + "processing_signature": "Processing signature...", + "applying_signature": "Applying signature...", + "processing_document": "Processing document..." } }, "notifications": { diff --git a/mobile/src/assets/locales/ro/translation.json b/mobile/src/assets/locales/ro/translation.json index bf1aa2d3..c0d2a1c3 100644 --- a/mobile/src/assets/locales/ro/translation.json +++ b/mobile/src/assets/locales/ro/translation.json @@ -806,7 +806,8 @@ "pending_documents": "Documente în așteptare", "approved_history": "Istoric contracte", "expired": "încheiat", - "rejected": "respins", + "rejected": "Respins", + "action_expired": "Acțiune expirată", "current": "curent", "contract": { "title": "Contract {{value}}", @@ -816,6 +817,7 @@ "approved": "Contractul a fost aprobat de ambele părți și nu mai poate fi modificat. Descarcă și vizualizează contractul.", "expired": "Perioada contractului a luat sfârșit. Descarcă și vizualizează contractul.", "rejected": "Contractul a fost respins.", + "action_expired": "Contractul nu a fost semnat în intervalul de timp dedicat.", "sign": "Semnează", "reject": "Respinge contract" }, @@ -866,6 +868,12 @@ "title": "A apărut o eroare la respingerea contractului" } } + }, + "loading_signature": { + "title": "Se semnează documentul...", + "processing_signature": "Se procesează semnătura...", + "applying_signature": "Se aplică semnătura...", + "processing_document": "Se procesează documentul..." } }, "notifications": { diff --git a/mobile/src/common/utils/document-contracts.helpers.ts b/mobile/src/common/utils/document-contracts.helpers.ts index b5f52755..db0fae4d 100644 --- a/mobile/src/common/utils/document-contracts.helpers.ts +++ b/mobile/src/common/utils/document-contracts.helpers.ts @@ -49,6 +49,14 @@ export const mapContractToColor = (contract: DocumentContract, t: any) => { info: `${t('rejected')}`, }; } + //action expired contract + if (contract.status === DocumentContractStatus.ACTION_EXPIRED) { + return { + color: 'color-danger-800', + backgroundColor: 'red-50', + info: `${t('action_expired')}`, + }; + } // expired contract if (isAfter(new Date(), new Date(contract.documentEndDate))) { return { @@ -89,6 +97,10 @@ export const renderContractInfoText = (contract: DocumentContract, t: any) => { ) { return t('contract.rejected'); } + // action expired contract + if (contract.status === DocumentContractStatus.ACTION_EXPIRED) { + return t('contract.action_expired'); + } // expired contract if (isAfter(new Date(), new Date(contract.documentEndDate))) { return t('contract.expired'); diff --git a/mobile/src/components/SignatureBottomSheet.tsx b/mobile/src/components/SignatureBottomSheet.tsx index 6da18adb..15c19e27 100644 --- a/mobile/src/components/SignatureBottomSheet.tsx +++ b/mobile/src/components/SignatureBottomSheet.tsx @@ -1,9 +1,8 @@ -import React from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet'; -import { StyleSheet } from 'react-native'; +import { Animated, StyleSheet, View } from 'react-native'; import { useReducedMotion } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { LoadingIndicator } from '../layouts/PageLayout'; import { SignatureScreen } from './SignatureScreen'; import { useTranslation } from 'react-i18next'; import successIcon from '../assets/svg/success-icon'; @@ -14,7 +13,7 @@ import upsIcon from '../assets/svg/ups-icon'; import { renderBackdrop } from '../components/BottomSheet'; import { BottomSheetMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; import { SignatureViewRef } from 'react-native-signature-canvas'; - +import LottieView from 'lottie-react-native'; interface SignatureBottomSheetProps { bottomSheetRef: React.RefObject; snapPoints: number[]; @@ -39,6 +38,75 @@ interface SignatureBottomSheetProps { handleClearLegalGuardianSignature: () => void; readLegalGuardianSignature: () => void; } +const LoadingScreen = () => { + const { t } = useTranslation('documents-contract'); + const theme = useTheme(); + const loadingTexts = useMemo( + () => [ + `${t('loading_signature.processing_signature')}`, + `${t('loading_signature.applying_signature')}`, + `${t('loading_signature.processing_document')}`, + ], + [t], + ); + const [currentText, setCurrentText] = useState(loadingTexts[0]); + const [fadeAnim] = useState(new Animated.Value(1)); + + // changing text animation + // 1. fade out current text + // 2. change text + // 3. fade in new text + useEffect(() => { + let index = 0; + const interval = setInterval(() => { + Animated.sequence([ + // fade out animation + Animated.timing(fadeAnim, { + toValue: 0, + duration: 750, + useNativeDriver: true, + }), + ]).start(() => { + // change displayed text + setCurrentText(loadingTexts[(index + 1) % loadingTexts.length]); + // start fade in animation + Animated.timing(fadeAnim, { + toValue: 1, + duration: 250, + useNativeDriver: true, + }).start(); + index = (index + 1) % loadingTexts.length; + }); + }, 3000); + + return () => clearInterval(interval); + }, [loadingTexts, fadeAnim]); + + return ( + + + {`${t('loading_signature.title')}`} + + + + {currentText} + + + ); +}; export const SignatureBottomSheet = ({ bottomSheetRef, @@ -65,6 +133,7 @@ export const SignatureBottomSheet = ({ const reducedMotion = useReducedMotion(); const insets = useSafeAreaInsets(); const theme = useTheme(); + return ( + ) : ( // FIRST SCREEN - VOLUNTEER SIGNATURE ) ) : isLoadingSignContract ? ( - + ) : ( // SECOND SCREEN - LEGAL GUARDIAN SIGNATURE { // bottom sheet const snapPoints = useMemo( - () => (isFinishedSigning.isFinished ? [1, 550] : [1, 700]), - [isFinishedSigning], + () => (isFinishedSigning.isFinished ? [1, 550] : isLoadingSignContract ? [1, 350] : [1, 700]), + [isFinishedSigning, isLoadingSignContract], ); const bottomSheetRef = useRef(null); diff --git a/mobile/src/screens/DocumentsContracts.tsx b/mobile/src/screens/DocumentsContracts.tsx index 1c55c861..6607411c 100644 --- a/mobile/src/screens/DocumentsContracts.tsx +++ b/mobile/src/screens/DocumentsContracts.tsx @@ -66,6 +66,16 @@ export const DocumentsContracts = ({ navigation }: any) => { [allContracts, isLoadingAllContracts], ); + const actionExpiredContracts = useMemo( + () => + allContracts && + !isLoadingAllContracts && + allContracts.items.filter( + (contract: DocumentContract) => contract.status === DocumentContractStatus.ACTION_EXPIRED, + ), + [allContracts, isLoadingAllContracts], + ); + // an activeContract exists if the current date is between the document start and end date const activeContractExists = useMemo( () => @@ -195,6 +205,26 @@ export const DocumentsContracts = ({ navigation }: any) => { ); })} + {actionExpiredContracts.map((item: DocumentContract, index: number) => { + return ( + + + } + rightIconName={'chevron-right'} + startDate={item.documentStartDate} + endDate={item.documentEndDate} + // todo: download contract + onPress={() => onContractPress(item)} + info={`${t('action_expired')}`} + /> + {index < actionExpiredContracts.length - 1 && } + + ); + })} )}