diff --git a/backend/src/migrations/1721632351165-BalanceSheetAndNonPoliticalAffiliation.ts b/backend/src/migrations/1721632351165-BalanceSheetAndNonPoliticalAffiliation.ts new file mode 100644 index 00000000..4e3f43de --- /dev/null +++ b/backend/src/migrations/1721632351165-BalanceSheetAndNonPoliticalAffiliation.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class BalanceSheetAndNonPoliticalAffiliation1721632351165 + implements MigrationInterface +{ + name = 'BalanceSheetAndNonPoliticalAffiliation1721632351165'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "organization_legal" ADD "balance_sheet_file" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "organization_legal" ADD "non_political_affiliation_file" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "organization_legal" DROP COLUMN "non_political_affiliation_file"`, + ); + await queryRunner.query( + `ALTER TABLE "organization_legal" DROP COLUMN "balance_sheet_file"`, + ); + } +} diff --git a/backend/src/modules/application/services/application.service.ts b/backend/src/modules/application/services/application.service.ts index b91ca1a0..901e8637 100644 --- a/backend/src/modules/application/services/application.service.ts +++ b/backend/src/modules/application/services/application.service.ts @@ -62,7 +62,6 @@ export class ApplicationService { `${APPLICATIONS_FILES_DIR}`, logo, FILE_TYPE.IMAGE, - createApplicationDto.name, ); createApplicationDto = { @@ -339,7 +338,6 @@ export class ApplicationService { `${APPLICATIONS_FILES_DIR}`, logo, FILE_TYPE.IMAGE, - application.name, ); applicationPayload = { diff --git a/backend/src/modules/organization/constants/errors.constants.ts b/backend/src/modules/organization/constants/errors.constants.ts index 47868e21..9c504819 100644 --- a/backend/src/modules/organization/constants/errors.constants.ts +++ b/backend/src/modules/organization/constants/errors.constants.ts @@ -43,6 +43,15 @@ export const ORGANIZATION_ERRORS = { message: 'Error while uploading the files', errorCode: 'ORG_010', }, + UPLOAD_NON_POLITICAL_AFFILIATION: { + message: 'Error while uploading the non political affiliation file', + errorCode: 'ORG_030', + }, + + UPLOAD_BALANCE_SHEET: { + message: 'Error while uploading the balance sheet file', + errorCode: 'ORG_031', + }, GET_REPORT: { message: 'Report not found', errorCode: 'ORG_011', @@ -68,6 +77,15 @@ export const ORGANIZATION_ERRORS = { message: 'Error while deleting the organization statute', errorCode: 'ORG_027', }, + NON_POLITICAL_AFFILIATION: { + message: + 'Error while deleting the organization non political affiliation file', + errorCode: 'ORG_027', + }, + BALANCE_SHEET: { + message: 'Error while deleting the organization balance sheet file', + errorCode: 'ORG_027', + }, }, ALREADY_RESTRICTED: { message: 'Organization is already RESTRICTED', diff --git a/backend/src/modules/organization/constants/files.constants.ts b/backend/src/modules/organization/constants/files.constants.ts index f6bd56f4..92d6e198 100644 --- a/backend/src/modules/organization/constants/files.constants.ts +++ b/backend/src/modules/organization/constants/files.constants.ts @@ -1,6 +1,8 @@ export const ORGANIZATION_FILES_DIR = { LOGO: 'logo', STATUTE: 'statute', + NON_POLITICAL_AFFILITION: 'non_political_affiliation', + BALANCE_SHEET: 'balance_sheet', REPORTS: 'reports', PARTNERS: 'partners', INVESTORS: 'investors', diff --git a/backend/src/modules/organization/controllers/organization-profile.controller.ts b/backend/src/modules/organization/controllers/organization-profile.controller.ts index 6b66635b..b279368c 100644 --- a/backend/src/modules/organization/controllers/organization-profile.controller.ts +++ b/backend/src/modules/organization/controllers/organization-profile.controller.ts @@ -55,6 +55,8 @@ export class OrganizationProfileController { FileFieldsInterceptor([ { name: 'logo', maxCount: 1 }, { name: 'organizationStatute', maxCount: 1 }, + { name: 'nonPoliticalAffiliationFile', maxCount: 1 }, + { name: 'balanceSheetFile', maxCount: 1 }, ]), ) @Patch() @@ -65,9 +67,13 @@ export class OrganizationProfileController { { logo, organizationStatute, + nonPoliticalAffiliationFile, + balanceSheetFile, }: { logo: Express.Multer.File[]; organizationStatute: Express.Multer.File[]; + nonPoliticalAffiliationFile: Express.Multer.File[]; + balanceSheetFile: Express.Multer.File[]; }, ) { return this.organizationService.update( @@ -75,6 +81,8 @@ export class OrganizationProfileController { updateOrganizationDto, logo, organizationStatute, + nonPoliticalAffiliationFile, + balanceSheetFile, ); } diff --git a/backend/src/modules/organization/controllers/organization.controller.ts b/backend/src/modules/organization/controllers/organization.controller.ts index 7e522468..eea98448 100644 --- a/backend/src/modules/organization/controllers/organization.controller.ts +++ b/backend/src/modules/organization/controllers/organization.controller.ts @@ -212,6 +212,8 @@ export class OrganizationController { FileFieldsInterceptor([ { name: 'logo', maxCount: 1 }, { name: 'organizationStatute', maxCount: 1 }, + { name: 'nonPoliticalAffiliationFile', maxCount: 1 }, + { name: 'balanceSheetFile', maxCount: 1 }, ]), ) @Patch(':id') @@ -222,9 +224,13 @@ export class OrganizationController { { logo, organizationStatute, + nonPoliticalAffiliationFile, + balanceSheetFile, }: { logo: Express.Multer.File[]; organizationStatute: Express.Multer.File[]; + nonPoliticalAffiliationFile: Express.Multer.File[]; + balanceSheetFile: Express.Multer.File[]; }, ) { return this.organizationService.update( @@ -232,6 +238,8 @@ export class OrganizationController { updateOrganizationDto, logo, organizationStatute, + nonPoliticalAffiliationFile, + balanceSheetFile, ); } @@ -321,6 +329,29 @@ export class OrganizationController { ); } + @Roles(Role.SUPER_ADMIN, Role.ADMIN) + @ApiParam({ name: 'id', type: String }) + @Delete(':id/non-political-affiliation') + deleteNonPoliticalAffiliation( + @ExtractUser() user: User, + @Param('id') id: number, + ) { + // for admin user.organizationId has precedence + return this.organizationService.deleteNonPoliticalAffiliation( + user?.organizationId || id, + ); + } + + @Roles(Role.SUPER_ADMIN, Role.ADMIN) + @ApiParam({ name: 'id', type: String }) + @Delete(':id/balance-sheet') + deleteBalanceSheet(@ExtractUser() user: User, @Param('id') id: number) { + // for admin user.organizationId has precedence + return this.organizationService.deleteBalanceSheet( + user?.organizationId || id, + ); + } + @Roles(Role.SUPER_ADMIN) @ApiParam({ name: 'id', type: String }) @ApiBody({ diff --git a/backend/src/modules/organization/dto/update-organization-legal.dto.ts b/backend/src/modules/organization/dto/update-organization-legal.dto.ts index a4165b46..c150d2da 100644 --- a/backend/src/modules/organization/dto/update-organization-legal.dto.ts +++ b/backend/src/modules/organization/dto/update-organization-legal.dto.ts @@ -28,4 +28,12 @@ export class UpdateOrganizationLegalDto { @IsOptional() @IsString() organizationStatute?: string; + + @IsOptional() + @IsString() + nonPoliticalAffiliationFile?: string; + + @IsOptional() + @IsString() + balanceSheetFile?: string; } diff --git a/backend/src/modules/organization/entities/organization-legal.entity.ts b/backend/src/modules/organization/entities/organization-legal.entity.ts index 821bfef8..1c286569 100644 --- a/backend/src/modules/organization/entities/organization-legal.entity.ts +++ b/backend/src/modules/organization/entities/organization-legal.entity.ts @@ -45,4 +45,18 @@ export class OrganizationLegal extends BaseEntity { @Column({ type: 'varchar', name: 'organization_statute', nullable: true }) organizationStatute: string; + + @Column({ + type: 'varchar', + name: 'non_political_affiliation_file', + nullable: true, + }) + nonPoliticalAffiliationFile: string; + + @Column({ + type: 'varchar', + name: 'balance_sheet_file', + nullable: true, + }) + balanceSheetFile: string; } diff --git a/backend/src/modules/organization/services/organization-legal.service.ts b/backend/src/modules/organization/services/organization-legal.service.ts index 525175d4..abcf3b71 100644 --- a/backend/src/modules/organization/services/organization-legal.service.ts +++ b/backend/src/modules/organization/services/organization-legal.service.ts @@ -13,6 +13,8 @@ import { ORGANIZATION_ERRORS } from '../constants/errors.constants'; import { UpdateOrganizationLegalDto } from '../dto/update-organization-legal.dto'; import { OrganizationLegalRepository } from '../repositories'; import { ContactService } from './contact.service'; +import { ORGANIZATION_FILES_DIR } from '../constants/files.constants'; +import * as Sentry from '@sentry/node'; @Injectable() export class OrganizationLegalService { @@ -26,8 +28,9 @@ export class OrganizationLegalService { public async update( id: number, updateOrganizationLegalDto: UpdateOrganizationLegalDto, - organizationStatutePath?: string, organizationStatute?: Express.Multer.File[], + nonPoliticalAffiliationFile?: Express.Multer.File[], + balanceSheetFile?: Express.Multer.File[], ) { const orgLegal = await this.organizationLegalRepostory.get({ where: { id }, @@ -62,7 +65,7 @@ export class OrganizationLegalService { try { const uploadedFile = await this.fileManagerService.uploadFiles( - organizationStatutePath, + `${id}/${ORGANIZATION_FILES_DIR.STATUTE}`, organizationStatute, FILE_TYPE.FILE, ); @@ -76,6 +79,7 @@ export class OrganizationLegalService { error: { error }, ...ORGANIZATION_ERRORS.UPLOAD, }); + Sentry.captureException(error); if (error instanceof HttpException) { throw error; } else { @@ -87,6 +91,76 @@ export class OrganizationLegalService { } } + // Non Political Affiliation File + if (nonPoliticalAffiliationFile) { + if (orgLegal.nonPoliticalAffiliationFile) { + await this.fileManagerService.deleteFiles([ + orgLegal.nonPoliticalAffiliationFile, + ]); + } + + try { + const uploadedFile = await this.fileManagerService.uploadFiles( + `${id}/${ORGANIZATION_FILES_DIR.NON_POLITICAL_AFFILITION}`, + nonPoliticalAffiliationFile, + FILE_TYPE.FILE, + ); + + organizationLegalData = { + ...organizationLegalData, + nonPoliticalAffiliationFile: uploadedFile[0], + }; + } catch (error) { + this.logger.error({ + error: { error }, + ...ORGANIZATION_ERRORS.UPLOAD_NON_POLITICAL_AFFILIATION, + }); + Sentry.captureException(error); + if (error instanceof HttpException) { + throw error; + } else { + throw new InternalServerErrorException({ + ...ORGANIZATION_ERRORS.UPLOAD_NON_POLITICAL_AFFILIATION, + error, + }); + } + } + } + + // Balance Sheet File + if (balanceSheetFile) { + if (orgLegal.balanceSheetFile) { + await this.fileManagerService.deleteFiles([orgLegal.balanceSheetFile]); + } + + try { + const uploadedFile = await this.fileManagerService.uploadFiles( + `${id}/${ORGANIZATION_FILES_DIR.BALANCE_SHEET}`, + balanceSheetFile, + FILE_TYPE.FILE, + ); + + organizationLegalData = { + ...organizationLegalData, + balanceSheetFile: uploadedFile[0], + }; + } catch (error) { + this.logger.error({ + error: { error }, + ...ORGANIZATION_ERRORS.UPLOAD_BALANCE_SHEET, + }); + Sentry.captureException(error); + if (error instanceof HttpException) { + throw error; + } else { + throw new InternalServerErrorException({ + ...ORGANIZATION_ERRORS.UPLOAD_BALANCE_SHEET, + error, + }); + } + } + } + await this.organizationLegalRepostory.save({ id, ...organizationLegalData, @@ -109,6 +183,28 @@ export class OrganizationLegalService { }; } + if (organizationLegal.nonPoliticalAffiliationFile) { + const nonPoliticalAffiliationFilePublicUrl = + await this.fileManagerService.generatePresignedURL( + organizationLegal.nonPoliticalAffiliationFile, + ); + organizationLegal = { + ...organizationLegal, + nonPoliticalAffiliationFile: nonPoliticalAffiliationFilePublicUrl, + }; + } + + if (organizationLegal.balanceSheetFile) { + const balanceSheetFile = + await this.fileManagerService.generatePresignedURL( + organizationLegal.balanceSheetFile, + ); + organizationLegal = { + ...organizationLegal, + balanceSheetFile: balanceSheetFile, + }; + } + return organizationLegal; } @@ -139,6 +235,7 @@ export class OrganizationLegalService { ...ORGANIZATION_ERRORS.DELETE.STATUTE, }); + Sentry.captureException(error); const err = error?.response; throw new InternalServerErrorException({ ...ORGANIZATION_ERRORS.DELETE.STATUTE, @@ -146,4 +243,75 @@ export class OrganizationLegalService { }); } } + + public async deleteNonPoliticalAffiliation( + organizationLegalId: number, + ): Promise { + try { + // 1. Query organization legal data + const organizationLegal = await this.organizationLegalRepostory.get({ + where: { id: organizationLegalId }, + }); + + if (organizationLegal?.nonPoliticalAffiliationFile) { + // 2. remove file from s3 + await this.fileManagerService.deleteFiles([ + organizationLegal.nonPoliticalAffiliationFile, + ]); + + // 3. remove path from database + await this.organizationLegalRepostory.save({ + ...organizationLegal, + nonPoliticalAffiliationFile: null, + }); + } + } catch (error) { + this.logger.error({ + error, + ...ORGANIZATION_ERRORS.DELETE.NON_POLITICAL_AFFILIATION, + }); + Sentry.captureException(error); + + const err = error?.response; + throw new InternalServerErrorException({ + ...ORGANIZATION_ERRORS.DELETE.NON_POLITICAL_AFFILIATION, + error: err, + }); + } + } + + public async deleteBalanceSheetFile( + organizationLegalId: number, + ): Promise { + try { + const organizationLegal = await this.organizationLegalRepostory.get({ + where: { id: organizationLegalId }, + }); + + if (organizationLegal?.balanceSheetFile) { + // 2. remove file from s3 + await this.fileManagerService.deleteFiles([ + organizationLegal.balanceSheetFile, + ]); + + // 3. remove path from database + await this.organizationLegalRepostory.save({ + ...organizationLegal, + balanceSheetFile: null, + }); + } + } catch (error) { + this.logger.error({ + error, + ...ORGANIZATION_ERRORS.DELETE.BALANCE_SHEET, + }); + + Sentry.captureException(error); + const err = error?.response; + throw new InternalServerErrorException({ + ...ORGANIZATION_ERRORS.DELETE.BALANCE_SHEET, + error: err, + }); + } + } } diff --git a/backend/src/modules/organization/services/organization-report.service.ts b/backend/src/modules/organization/services/organization-report.service.ts index 5e36a14f..cd3d7bf9 100644 --- a/backend/src/modules/organization/services/organization-report.service.ts +++ b/backend/src/modules/organization/services/organization-report.service.ts @@ -107,7 +107,6 @@ export class OrganizationReportService { `${organizationId}/${ORGANIZATION_FILES_DIR.PARTNERS}`, files, FILE_TYPE.FILE, - `${partner.year}_${PARTNER_LIST}`, ); await this.partnerRepository.save({ @@ -158,7 +157,6 @@ export class OrganizationReportService { `${organizationId}/${ORGANIZATION_FILES_DIR.INVESTORS}`, files, FILE_TYPE.FILE, - `${investor.year}_${INVESTOR_LIST}`, ); await this.investorRepository.save({ diff --git a/backend/src/modules/organization/services/organization.service.ts b/backend/src/modules/organization/services/organization.service.ts index f488d14c..9cb4328f 100644 --- a/backend/src/modules/organization/services/organization.service.ts +++ b/backend/src/modules/organization/services/organization.service.ts @@ -589,6 +589,8 @@ export class OrganizationService { } // Public: The URL can be replaced as above OR move all public in the "public" folder of each organization for better structure + + // Statute if (organization.organizationLegal.organizationStatute) { const organizationStatute = await this.fileManagerService.generatePresignedURL( @@ -597,6 +599,25 @@ export class OrganizationService { organization.organizationLegal.organizationStatute = organizationStatute; } + // Non Political Affiliation File + if (organization.organizationLegal.nonPoliticalAffiliationFile) { + const nonPoliticalAffiliationFile = + await this.fileManagerService.generatePresignedURL( + organization.organizationLegal.nonPoliticalAffiliationFile, + ); + organization.organizationLegal.nonPoliticalAffiliationFile = + nonPoliticalAffiliationFile; + } + + // Balance Sheet File + if (organization.organizationLegal.balanceSheetFile) { + const balanceSheetFile = + await this.fileManagerService.generatePresignedURL( + organization.organizationLegal.balanceSheetFile, + ); + organization.organizationLegal.balanceSheetFile = balanceSheetFile; + } + return organization; } @@ -961,6 +982,8 @@ export class OrganizationService { updateOrganizationDto: UpdateOrganizationDto, logo?: Express.Multer.File[], organizationStatute?: Express.Multer.File[], + nonPoliticalAffiliationFile?: Express.Multer.File[], + balanceSheetFile?: Express.Multer.File[], ): Promise { const organization = await this.find(id); @@ -986,11 +1009,19 @@ export class OrganizationService { FILE_TYPE.FILE, ); + this.fileManagerService.validateFiles( + nonPoliticalAffiliationFile, + FILE_TYPE.FILE, + ); + + this.fileManagerService.validateFiles(balanceSheetFile, FILE_TYPE.FILE); + return this.organizationLegalService.update( organization.organizationLegalId, updateOrganizationDto.legal, - `${id}/${ORGANIZATION_FILES_DIR.STATUTE}`, organizationStatute, + nonPoliticalAffiliationFile, + balanceSheetFile, ); } @@ -1155,6 +1186,64 @@ export class OrganizationService { } } + public async deleteNonPoliticalAffiliation( + organizationId: number, + ): Promise { + try { + const organization = await this.organizationRepository.get({ + where: { id: organizationId }, + relations: ['organizationLegal'], + }); + + await this.organizationLegalService.deleteNonPoliticalAffiliation( + organization.organizationLegalId, + ); + } catch (error) { + this.logger.error({ + error, + ...ORGANIZATION_ERRORS.DELETE.NON_POLITICAL_AFFILIATION, + }); + + if (error instanceof HttpException) { + throw error; + } else { + const err = error?.response; + throw new InternalServerErrorException({ + ...ORGANIZATION_ERRORS.DELETE.NON_POLITICAL_AFFILIATION, + error: err, + }); + } + } + } + + public async deleteBalanceSheet(organizationId: number): Promise { + try { + const organization = await this.organizationRepository.get({ + where: { id: organizationId }, + relations: ['organizationLegal'], + }); + + await this.organizationLegalService.deleteBalanceSheetFile( + organization.organizationLegalId, + ); + } catch (error) { + this.logger.error({ + error, + ...ORGANIZATION_ERRORS.DELETE.BALANCE_SHEET, + }); + + if (error instanceof HttpException) { + throw error; + } else { + const err = error?.response; + throw new InternalServerErrorException({ + ...ORGANIZATION_ERRORS.DELETE.BALANCE_SHEET, + error: err, + }); + } + } + } + /** * Will update the status from PENDING to ACTIVE * diff --git a/backend/src/shared/services/s3-file-manager.service.ts b/backend/src/shared/services/s3-file-manager.service.ts index 0dee77d4..2c713998 100644 --- a/backend/src/shared/services/s3-file-manager.service.ts +++ b/backend/src/shared/services/s3-file-manager.service.ts @@ -95,17 +95,18 @@ export class S3FileManagerService { path: string, files: Express.Multer.File[], fileType: FILE_TYPE, - fileName?: string, ): Promise { this.logger.log(`Preparing to upload ${files.length} files...`); this.validateFiles(files, fileType); + const extension = files[0].originalname.split('.').pop(); + // Create upload params const params: FileUploadParams[] = files.map((file) => ({ Body: file.buffer, Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), - Key: `${path}/${uuid()}`, + Key: `${path}/${uuid()}.${extension}`, })); // Prepare upload diff --git a/frontend/src/assets/locales/en/translation.json b/frontend/src/assets/locales/en/translation.json index 1095da4c..c8242473 100644 --- a/frontend/src/assets/locales/en/translation.json +++ b/frontend/src/assets/locales/en/translation.json @@ -494,9 +494,22 @@ "other_information": "This information will be public, be careful what you share.", "statute": "The status of the organisation", "statute_information": "This information will be public, be careful what you share.", - "document": "Document", + "no_document": "No document uploaded", "file_name": "Statute_Organisation", "statute_upload": "Upload file", + + "non_political_affiliation": "Declaration of non-political affiliation", + "non_political_affiliation_information": "Upload the self-declaration by the legal representative of the organization stating that the president and members of the Board of Directors are not part of the leadership of a political party and/or do not hold any public office. This declaration will not be displayed publicly. The document is only used to validate the organization's eligibility to access certain solutions in NGO Hub (such as Vot ONG and RO Help). Accepted file formats: pdf, maximum 25 MB", + "non_political_affiliation_no_document": "No document uploaded", + "non_political_affiliation_file_name": "Declaration of non-political affiliation", + "non_political_affiliation_upload": "Upload file", + + "balance_sheet": "Balance Sheet", + "balance_sheet_information": "Upload the latest balance sheet submitted to the Ministry of Finance. This will not be displayed publicly. The document is only used to validate the organization's eligibility to access certain solutions in NGO Hub (such as Vot ONG and RO Help). Accepted file formats: pdf, maximum 50 MB", + "balance_sheet_no_document": "No document uploaded", + "balance_sheet_file_name": "Balance Sheet", + "balance_sheet_upload": "Upload file", + "modal": { "add": "Add", "edit_director": "Editing of the Board member", @@ -518,6 +531,16 @@ "title": "Are you sure you want to delete the Organisation's Constitution?", "description": "TODO#1: Lorem ipsum." }, + + "delete_non_politicial_affiliation_modal": { + "title": "Are you sure you want to delete the Declaration of non-political affiliation?", + "description": "" + }, + + "delete_balance_sheet_modal": { + "title": "Are you sure you want to delete the Balance Sheet?", + "description": "" + }, "header": { "name": "Full name", "role": "Role", diff --git a/frontend/src/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json index 24128a52..46676876 100644 --- a/frontend/src/assets/locales/ro/translation.json +++ b/frontend/src/assets/locales/ro/translation.json @@ -497,9 +497,23 @@ "other_information": "Numele persoanelor adăugate sunt pentru a ușura comunicarea cu diferite persoane din organizația ta. De exemplu, aici poți adăuga managerul de comunicare sau persoana de fundraising dedicată din ONG-ul tău, sau coordonatori de sucursale din alte orașe. Datele de contact ale persoanelor relevante nu vor fi afișate public.", "statute": "Statutul organizației", "statute_information": "Încarcă statutul anonimizat. Acesta nu va fi afișat public. Documentul este folosit doar pentru a valida statutul de organizație neguvernamentală, și a confirma că scopul și misiunea organizației nu contravin cu criteliile de eligibilitate (ca de exemplu organizația pe care o reprezinți nu desfășoară activități care să încalce drepturile omului, nu este afiliată unui partid politic, nu este o asociație de locatari, etc.) Fișiere acceptate: pdf, maxim 25 MB.", - "document": "Document", + + "no_document": "Nu există niciun document adăugat", "file_name": "Statut_Organizație", "statute_upload": "Încarcă fișier", + + "non_political_affiliation": "Declarație de neapartenență politică", + "non_political_affiliation_information": "Încarcă declarația pe proprie răspundere prin care reprezentantul legal al organizației declară că președintele și membrii Consiliul Director nu fac parte din conducerea unui partid politic și/sau nu dețin nicio funcție publică. Această declarație nu va fi afișată public. Documentul este folosit doar pentru a valida eligibilitatea organizației de a accesa anumite soluții din NGO Hub (ca de exemplu Vot ONG și RO Help). Fișiere acceptate: pdf, maxim 25 MB", + "non_political_affiliation_no_document": "Nu există niciun document adăugat", + "non_political_affiliation_file_name": "Declarație de neapartenență politică", + "non_political_affiliation_upload": "Încarcă fișier", + + "balance_sheet": "Bilanț contabil", + "balance_sheet_information": "Încarcă ultimul bilanțul contabil depus la Ministerul de Finanțe. Acesta nu va fi afișat public. Documentul este folosit doar pentru a valida eligibilitatea organizației de a accesa anumite soluții din NGO Hub (ca de exemplu Vot ONG și RO Help). Fișiere acceptate: pdf, maxim 50 MB", + "balance_sheet_no_document": "Nu există niciun document adăugat", + "balance_sheet_file_name": "Bilanț contabil", + "balance_sheet_upload": "Încarcă fișier", + "modal": { "add": "Adaugă", "edit_director": "Editare membru Consiliu Director", @@ -521,6 +535,16 @@ "title": "Ești sigur ca dorești ștergerea Statutului Organizației?", "description": "În cazul ștergerii statutului, un nou statut trebuie încărcat în maxim 30 de zile pentru a menține activ contul organizației în NGO Hub." }, + + "delete_non_politicial_affiliation_modal": { + "title": "Ești sigur ca dorești ștergerea Declarației de neapartenență politică?", + "description": "" + }, + + "delete_balance_sheet_modal": { + "title": "Ești sigur ca dorești ștergerea Bilanțului contabil?", + "description": "" + }, "header": { "name": "Nume și prenume", "role": "Rol", diff --git a/frontend/src/common/constants/file.constants.ts b/frontend/src/common/constants/file.constants.ts index 3d0bfc3c..8403b28a 100644 --- a/frontend/src/common/constants/file.constants.ts +++ b/frontend/src/common/constants/file.constants.ts @@ -1,5 +1,7 @@ export const FILE_TYPES_ACCEPT = { LOGO: 'image/png, image/jpeg, image/svg', STATUTE: '.pdf', + NON_POLITICAL_AFFILIATION: '.pdf', + BALANCE_SHEET: '.pdf', EXCEL: '.xls,.xlsx', }; diff --git a/frontend/src/pages/organization/components/OrganizationLegal/OrganizationLegal.tsx b/frontend/src/pages/organization/components/OrganizationLegal/OrganizationLegal.tsx index a2edc36a..a722978e 100644 --- a/frontend/src/pages/organization/components/OrganizationLegal/OrganizationLegal.tsx +++ b/frontend/src/pages/organization/components/OrganizationLegal/OrganizationLegal.tsx @@ -18,7 +18,7 @@ import PopoverMenu, { PopoverMenuRowType } from '../../../../components/popover- import SectionHeader from '../../../../components/section-header/SectionHeader'; import Spinner from '../../../../components/spinner/Spinner'; import { AuthContext } from '../../../../contexts/AuthContext'; -import { useDeleteOrganizationStatuteMutation } from '../../../../services/organization/Organization.queries'; +import { useDeleteBalanceSheetFileMutation, useDeleteNonPoliticalAffiliationFileMutation, useDeleteOrganizationStatuteMutation } from '../../../../services/organization/Organization.queries'; import { useSelectedOrganization } from '../../../../store/selectors'; import { UserRole } from '../../../users/enums/UserRole.enum'; import { Contact } from '../../interfaces/Contact.interface'; @@ -32,6 +32,8 @@ import { OthersTableHeaders } from './table-headers/OthersTable.headers'; const OrganizationLegal = () => { const [isEditMode, setEditMode] = useState(false); const [organizationStatute, setOrganizationStatute] = useState(null); + const [nonPoliticalAffiliationFile, setNonPoliticalAffiliationFile] = useState(null); + const [balanceSheetFile, setBalanceSheetFile] = useState(null); // directors const [directors, setDirectors] = useState[]>([]); const [directorsDeleted, setDirectorsDeleted] = useState([]); @@ -44,14 +46,28 @@ const OrganizationLegal = () => { const [isDeleteOtheModalOpen, setIsDeleteOtherModalOpen] = useState(false); const [isDeleteOrganizationStatuteModalOpen, setDeleteOrganizationStatuteModalOpen] = useState(false); + + const [isDeleteNonPoliticalAffiliationFileModalOpen, setDeleteNonPoliticalAffiliationFileModalOpen] = + useState(false); + + const [isDeleteBalanceSheetFileModalOpen, setDeleteBalanceSheetFileModalOpen] = + useState(false); + const [selectedOther, setSelectedOther] = useState | null>(null); // queries const { organizationLegal, organization } = useSelectedOrganization(); const { updateOrganization, isLoading: isLoadingUpdateOrganization } = useOutletContext(); + + const { mutate: deleteOrganizationStatute, isLoading: isRemovingOrganizationStatute } = useDeleteOrganizationStatuteMutation(); + const { mutate: deleteNonPoliticalAffiliationFile, isLoading: isRemovingNonPoliticalAffiliationFile } = + useDeleteNonPoliticalAffiliationFileMutation(); + + const { mutateAsync: deleteBalanceSheetFile, isLoading: isRemovingBalanceSheetFile } = useDeleteBalanceSheetFileMutation() + const { role } = useContext(AuthContext); // React i18n const { t } = useTranslation(['legal', 'organization', 'common']); @@ -174,6 +190,18 @@ const OrganizationLegal = () => { setDeleteOrganizationStatuteModalOpen(true); }; + const onDeleteNonPoliticalAffiliationFile = (event: any) => { + event.stopPropagation(); + event.preventDefault(); + setDeleteNonPoliticalAffiliationFileModalOpen(true); + }; + + const onDeleteBalanceSheetFile = (event: any) => { + event.stopPropagation(); + event.preventDefault(); + setDeleteBalanceSheetFileModalOpen(true); + }; + const onOpenDeleteOtherModal = (row: Person) => { setSelectedOther(row); setIsDeleteOtherModalOpen(true); @@ -227,10 +255,14 @@ const OrganizationLegal = () => { organization: { legal: { legalReprezentative, directors, directorsDeleted, others } }, logo: null, organizationStatute, + nonPoliticalAffiliationFile, + balanceSheetFile }, { onSuccess: () => { setOrganizationStatute(null); + setNonPoliticalAffiliationFile(null); + setBalanceSheetFile(null); setEditMode(false); }, onError: (error: any) => { @@ -239,6 +271,8 @@ const OrganizationLegal = () => { if (err.code) { useErrorToast(FILE_ERRORS[err.code]); setOrganizationStatute(null); + setNonPoliticalAffiliationFile(null); + setBalanceSheetFile(null); } else { useErrorToast(t('save_error', { ns: 'organization' })); } @@ -247,7 +281,7 @@ const OrganizationLegal = () => { ); }; - const onChangeFile = (event: React.ChangeEvent) => { + const onChangeStatuteFile = (event: React.ChangeEvent) => { if (event.target.files && event.target.files.length > 0) { setOrganizationStatute(event.target.files[0]); event.target.value = ''; @@ -256,6 +290,25 @@ const OrganizationLegal = () => { } }; + const onChangeNonPoliticalAffiliationFile = (event: React.ChangeEvent) => { + if (event.target.files && event.target.files.length > 0) { + setNonPoliticalAffiliationFile(event.target.files[0]); + event.target.value = ''; + } else { + event.target.value = ''; + } + }; + + + const onChangeBalanceSheetFile = (event: React.ChangeEvent) => { + if (event.target.files && event.target.files.length > 0) { + setBalanceSheetFile(event.target.files[0]); + event.target.value = ''; + } else { + event.target.value = ''; + } + }; + const onRemoveOrganizationStatute = () => { // 1. check if we have a path in s3 and remove it if (organizationLegal?.organizationStatute) { @@ -278,6 +331,50 @@ const OrganizationLegal = () => { } }; + const onRemoveNonPoliticalAffiliationFile = () => { + // 1. check if we have a path in s3 and remove it + if (organizationLegal?.nonPoliticalAffiliationFile) { + deleteNonPoliticalAffiliationFile( + { organizationId: organization?.id as number }, + { + onError: (error: any) => { + useErrorToast(FILE_ERRORS[error?.response.data.code]); + }, + onSettled: () => { + setNonPoliticalAffiliationFile(null); + setDeleteNonPoliticalAffiliationFileModalOpen(false); + }, + }, + ); + } else { + // 2. the file is only in memory so we clear state + setNonPoliticalAffiliationFile(null); + setDeleteNonPoliticalAffiliationFileModalOpen(false); + } + }; + + const onRemoveBalanceSheetFile = () => { + // 1. check if we have a path in s3 and remove it + if (organizationLegal?.balanceSheetFile) { + deleteBalanceSheetFile( + { organizationId: organization?.id as number }, + { + onError: (error: any) => { + useErrorToast(FILE_ERRORS[error?.response.data.code]); + }, + onSettled: () => { + setBalanceSheetFile(null); + setDeleteBalanceSheetFileModalOpen(false); + }, + }, + ); + } else { + // 2. the file is only in memory so we clear state + setBalanceSheetFile(null); + setDeleteBalanceSheetFileModalOpen(false); + } + }; + return (
@@ -296,8 +393,8 @@ const OrganizationLegal = () => { !isEditMode ? setEditMode.bind(null, true) : () => { - handleSubmit(handleSave)(); - } + handleSubmit(handleSave)(); + } } > {isLoadingUpdateOrganization ? ( @@ -382,10 +479,17 @@ const OrganizationLegal = () => { )} + { + // Statute section + }
-

{t('document')}

+ {!organizationLegal?.organizationStatute && + organizationStatute === null && ( +

{t('no_document')}

+ ) + } {isEditMode && !organizationLegal?.organizationStatute && organizationStatute === null && ( @@ -402,7 +506,7 @@ const OrganizationLegal = () => { id="uploadStatute" type="file" accept={FILE_TYPES_ACCEPT.STATUTE} - onChange={onChangeFile} + onChange={onChangeStatuteFile} /> {!organizationStatute && isSubmitted && (

{ )}

+ + { + // Political Affiliation section + } +
+ +
+ {!organizationLegal?.nonPoliticalAffiliationFile && + nonPoliticalAffiliationFile === null && ( +

{t('non_political_affiliation_no_document')}

+ )} + {isEditMode && + !organizationLegal?.nonPoliticalAffiliationFile && + nonPoliticalAffiliationFile === null && ( +
+ + +
+ )} + {(organizationLegal?.nonPoliticalAffiliationFile || nonPoliticalAffiliationFile) && ( + + + {t('non_political_affiliation_file_name')} + {isEditMode && !isRemovingNonPoliticalAffiliationFile && ( + + )} + {isRemovingNonPoliticalAffiliationFile && } + + )} +
+
+ + {/* Balance Sheet File */} +
+ +
+ {!organizationLegal?.balanceSheetFile && + balanceSheetFile === null && ( +

{t('balance_sheet_no_document')}

+ )} + {isEditMode && + !organizationLegal?.balanceSheetFile && + balanceSheetFile === null && ( +
+ + +
+ )} + {(organizationLegal?.balanceSheetFile || balanceSheetFile) && ( + + + {t('balance_sheet_file_name')} + {isEditMode && !isRemovingBalanceSheetFile && ( + + )} + {isRemovingBalanceSheetFile && } + + )} +
+
{isDirectorModalOpen && ( { onConfirm={onRemoveOrganizationStatute} /> )} + {isDeleteNonPoliticalAffiliationFileModalOpen && ( + { + setDeleteNonPoliticalAffiliationFileModalOpen(false); + }} + onConfirm={onRemoveNonPoliticalAffiliationFile} + /> + )} + + {isDeleteBalanceSheetFileModalOpen && ( + { + setDeleteBalanceSheetFileModalOpen(false); + }} + onConfirm={onRemoveBalanceSheetFile} + /> + )}
diff --git a/frontend/src/pages/organization/interfaces/OrganizationLegal.interface.ts b/frontend/src/pages/organization/interfaces/OrganizationLegal.interface.ts index f90c529d..ccf7d4fe 100644 --- a/frontend/src/pages/organization/interfaces/OrganizationLegal.interface.ts +++ b/frontend/src/pages/organization/interfaces/OrganizationLegal.interface.ts @@ -7,4 +7,6 @@ export interface IOrganizationLegal extends BaseEntity { directors: Contact[]; others: Person[]; organizationStatute?: string; + nonPoliticalAffiliationFile?: string; + balanceSheetFile?: string; } diff --git a/frontend/src/services/organization/Organization.queries.ts b/frontend/src/services/organization/Organization.queries.ts index 0c0eaf39..79c56ec7 100644 --- a/frontend/src/services/organization/Organization.queries.ts +++ b/frontend/src/services/organization/Organization.queries.ts @@ -20,8 +20,10 @@ import { useSelectedOrganization } from '../../store/selectors'; import useStore from '../../store/store'; import { activateOrganization, + deleteBalanceSheetFile, deleteInvestors, deleteInvestorsByProfile, + deleteNonPolicalAffiliationFile, deleteOrganizationStatute, deletePartners, deletePartnersByProfile, @@ -62,6 +64,8 @@ interface OrganizationPayload { }; logo?: File | null; organizationStatute?: File | null; + nonPoliticalAffiliationFile?: File | null; + balanceSheetFile?: File | null; } /**SUPER ADMIN */ @@ -157,8 +161,22 @@ export const useOrganizationMutation = () => { } = useStore(); const { organizationFinancial } = useSelectedOrganization(); return useMutation( - ({ id, organization, logo, organizationStatute }: OrganizationPayload) => - patchOrganization(id as number, organization, logo, organizationStatute), + ({ + id, + organization, + logo, + organizationStatute, + nonPoliticalAffiliationFile, + balanceSheetFile, + }: OrganizationPayload) => + patchOrganization( + id as number, + organization, + logo, + organizationStatute, + nonPoliticalAffiliationFile, + balanceSheetFile, + ), { onSuccess: ( data: @@ -278,8 +296,20 @@ export const useOrganizationByProfileMutation = () => { } = useStore(); const { organizationFinancial } = useSelectedOrganization(); return useMutation( - ({ organization, logo, organizationStatute }: OrganizationPayload) => - patchOrganizationByProfile(organization, logo, organizationStatute), + ({ + organization, + logo, + organizationStatute, + nonPoliticalAffiliationFile, + balanceSheetFile, + }: OrganizationPayload) => + patchOrganizationByProfile( + organization, + logo, + organizationStatute, + nonPoliticalAffiliationFile, + balanceSheetFile, + ), { onSuccess: ( data: @@ -372,6 +402,37 @@ export const useDeleteOrganizationStatuteMutation = () => { ); }; +export const useDeleteNonPoliticalAffiliationFileMutation = () => { + const { setOrganizationLegal, organizationLegal } = useStore(); + return useMutation( + ({ organizationId }: { organizationId: number }) => + deleteNonPolicalAffiliationFile(organizationId), + { + onSuccess: () => { + setOrganizationLegal({ + ...(organizationLegal as IOrganizationLegal), + nonPoliticalAffiliationFile: undefined, + }); + }, + }, + ); +}; + +export const useDeleteBalanceSheetFileMutation = () => { + const { setOrganizationLegal, organizationLegal } = useStore(); + return useMutation( + ({ organizationId }: { organizationId: number }) => deleteBalanceSheetFile(organizationId), + { + onSuccess: () => { + setOrganizationLegal({ + ...(organizationLegal as IOrganizationLegal), + balanceSheetFile: undefined, + }); + }, + }, + ); +}; + export const useRetryAnafFinancialMutation = () => { const { setOrganizationFinancial } = useStore(); return useMutation( diff --git a/frontend/src/services/organization/Organization.service.ts b/frontend/src/services/organization/Organization.service.ts index db707dd9..690054c2 100644 --- a/frontend/src/services/organization/Organization.service.ts +++ b/frontend/src/services/organization/Organization.service.ts @@ -27,8 +27,16 @@ export const patchOrganizationByProfile = ( update: any, logo?: File | null, organizationStatute?: File | null, + nonPoliticalAffiliationFile?: File | null, + balanceSheetFile?: File | null, ): Promise => { - const payload = generateOrganizationFormDataPayload(update, logo, organizationStatute); + const payload = generateOrganizationFormDataPayload( + update, + logo, + organizationStatute, + nonPoliticalAffiliationFile, + balanceSheetFile, + ); return API.patch(`/organization-profile`, payload, { headers: { 'Content-Type': 'multipart/form-data' }, }).then((res) => res.data); @@ -128,8 +136,16 @@ export const patchOrganization = ( update: any, logo?: File | null, organizationStatute?: File | null, + nonPoliticalAffiliationFile?: File | null, + balanceSheetFile?: File | null, ): Promise => { - const payload = generateOrganizationFormDataPayload(update, logo, organizationStatute); + const payload = generateOrganizationFormDataPayload( + update, + logo, + organizationStatute, + nonPoliticalAffiliationFile, + balanceSheetFile, + ); return API.patch(`/organization/${id}`, payload, { headers: { 'Content-Type': 'multipart/form-data' }, }).then((res) => res.data); @@ -167,10 +183,22 @@ export const deleteOrganizationStatute = (organizationId: number) => { return API.delete(`organization/${organizationId}/statute`).then((res) => res.data); }; +export const deleteNonPolicalAffiliationFile = (organizationId: number) => { + return API.delete(`organization/${organizationId}/non-political-affiliation`).then( + (res) => res.data, + ); +}; + +export const deleteBalanceSheetFile = (organizationId: number) => { + return API.delete(`organization/${organizationId}/balance-sheet`).then((res) => res.data); +}; + const generateOrganizationFormDataPayload = ( update: any, logo?: File | null, organizationStatute?: File | null, + nonPoliticalAffiliationFile?: File | null, + balanceSheetFile?: File | null, ) => { let payload = new FormData(); @@ -203,5 +231,13 @@ const generateOrganizationFormDataPayload = ( payload.append('organizationStatute', organizationStatute); } + if (nonPoliticalAffiliationFile) { + payload.append('nonPoliticalAffiliationFile', nonPoliticalAffiliationFile); + } + + if (balanceSheetFile) { + payload.append('balanceSheetFile', balanceSheetFile); + } + return payload; };