From 642872a142b0d7675379d323c343fc03770862d8 Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Fri, 26 Jul 2024 13:21:44 +0300 Subject: [PATCH 01/57] feat: [438] yearly job to add financial reports, reports, partners, investors for the previous year --- backend/package-lock.json | 40 +++++++++++++ backend/package.json | 1 + backend/src/app.module.ts | 2 + .../src/common/config/email-config.service.ts | 2 +- .../entities/organization-general.entity.ts | 2 +- .../organization/organization.module.ts | 2 + .../organization/services/crons.service.ts | 60 +++++++++++++++++++ .../organization-financial.service.ts | 52 +++++++++++++++- .../services/organization-report.service.ts | 54 ++++++++++++++++- backend/src/shared/services/anaf.service.ts | 7 +++ 10 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 backend/src/modules/organization/services/crons.service.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index ccdaad3c5..b954f6ff3 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -23,6 +23,7 @@ "@nestjs/event-emitter": "2.0.4", "@nestjs/passport": "10.0.3", "@nestjs/platform-express": "10.3.10", + "@nestjs/schedule": "^4.1.0", "@nestjs/swagger": "7.3.1", "@nestjs/throttler": "5.2.0", "@nestjs/typeorm": "10.0.2", @@ -4741,6 +4742,31 @@ "@nestjs/core": "^10.0.0" } }, + "node_modules/@nestjs/schedule": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.0.tgz", + "integrity": "sha512-WEc96WTXZW+VI/Ng+uBpiBUwm6TWtAbQ4RKWkfbmzKvmbRGzA/9k/UyAWDS9k0pp+ZcbC+MaZQtt7TjQHrwX6g==", + "dependencies": { + "cron": "3.1.7", + "uuid": "10.0.0" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/@nestjs/schedule/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@nestjs/schematics": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.1.tgz", @@ -6461,6 +6487,11 @@ "@types/koa": "*" } }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -8656,6 +8687,15 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "devOptional": true }, + "node_modules/cron": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/cron/-/cron-3.1.7.tgz", + "integrity": "sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==", + "dependencies": { + "@types/luxon": "~3.4.0", + "luxon": "~3.4.0" + } + }, "node_modules/cron-parser": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", diff --git a/backend/package.json b/backend/package.json index e22289192..fe626a9d1 100644 --- a/backend/package.json +++ b/backend/package.json @@ -44,6 +44,7 @@ "@nestjs/event-emitter": "2.0.4", "@nestjs/passport": "10.0.3", "@nestjs/platform-express": "10.3.10", + "@nestjs/schedule": "^4.1.0", "@nestjs/swagger": "7.3.1", "@nestjs/throttler": "5.2.0", "@nestjs/typeorm": "10.0.2", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 6daa9c350..659f6626a 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -27,6 +27,7 @@ import { PracticeProgramModule } from './modules/practice-program/practice-progr import { CivicCenterModule } from './modules/civic-center-service/civic-center.module'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { NotificationsModule } from './modules/notifications/notifications.module'; +import { ScheduleModule } from '@nestjs/schedule'; @Module({ imports: [ @@ -37,6 +38,7 @@ import { NotificationsModule } from './modules/notifications/notifications.modul useClass: RateLimiterConfigService, }), EventEmitterModule.forRoot(), + ScheduleModule.forRoot(), // Providers DatabaseProviderModule, diff --git a/backend/src/common/config/email-config.service.ts b/backend/src/common/config/email-config.service.ts index 7e5a3984c..00031311a 100644 --- a/backend/src/common/config/email-config.service.ts +++ b/backend/src/common/config/email-config.service.ts @@ -22,7 +22,7 @@ export class EmailConfigService { defaults: { from: '"No Reply" ', }, - preview: true, + preview: false, template: { dir: __dirname + '/../../mail/templates', adapter: new HandlebarsAdapter({ asset_url: this.createAssetUrl }), diff --git a/backend/src/modules/organization/entities/organization-general.entity.ts b/backend/src/modules/organization/entities/organization-general.entity.ts index 38add69db..c0cd964af 100644 --- a/backend/src/modules/organization/entities/organization-general.entity.ts +++ b/backend/src/modules/organization/entities/organization-general.entity.ts @@ -39,7 +39,7 @@ export class OrganizationGeneral extends BaseEntity { @Column({ type: 'text', name: 'description', nullable: true }) description: string; - @Column({ type: 'text', name: 'address', nullable: false, default: '' }) + @Column({ type: 'text', name: 'address', nullable: true, default: '' }) address: string; @Column({ type: 'text', name: 'logo', nullable: true }) diff --git a/backend/src/modules/organization/organization.module.ts b/backend/src/modules/organization/organization.module.ts index d1f400721..46d8127ef 100644 --- a/backend/src/modules/organization/organization.module.ts +++ b/backend/src/modules/organization/organization.module.ts @@ -45,6 +45,7 @@ import { OrganizationHistory } from './entities/organization-history.entity'; import { OrganizationRequestHistory } from './entities/organization-request-history.entity'; import { PracticeProgramModule } from '../practice-program/practice-program.module'; import { CivicCenterModule } from '../civic-center-service/civic-center.module'; +import { OrganizationCronsService } from './services/crons.service'; @Module({ imports: [ @@ -90,6 +91,7 @@ import { CivicCenterModule } from '../civic-center-service/civic-center.module'; OrganizationViewRepository, OrganizationRequestRepository, OrganizationRequestService, + OrganizationCronsService, ], exports: [OrganizationService, OrganizationRequestService], }) diff --git a/backend/src/modules/organization/services/crons.service.ts b/backend/src/modules/organization/services/crons.service.ts new file mode 100644 index 000000000..9c45c4087 --- /dev/null +++ b/backend/src/modules/organization/services/crons.service.ts @@ -0,0 +1,60 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { OrganizationRepository } from '../repositories/organization.repository'; +import { OrganizationFinancialRepository } from '../repositories'; +import { OrganizationFinancialService } from './organization-financial.service'; +import { OrganizationReportService } from './organization-report.service'; +import * as Sentry from '@sentry/node'; +import { Cron, CronExpression } from '@nestjs/schedule'; + +@Injectable() +export class OrganizationCronsService { + private readonly logger = new Logger(OrganizationCronsService.name); + + constructor( + private readonly organizationRepository: OrganizationRepository, + private readonly organizationFinancialService: OrganizationFinancialService, + private readonly organizationReportService: OrganizationReportService, + ) {} + + @Cron('0 0 5 1 1 *') // 1st of January, 5:00 AM + async generateFinancialDataAndReportsForPreviousYear() { + const lastYear = new Date().getFullYear() - 1; + + // 1. Get all organizations with are missing the previous year the financial data and reports + const organizations = await this.organizationRepository.getMany({ + relations: { + organizationFinancial: true, + organizationGeneral: true, + organizationReport: { + reports: true, + partners: true, + investors: true, + }, + }, + }); + + for (const org of organizations) { + try { + // 2. Generate the financial reports + await this.organizationFinancialService.generateNewReports({ + organization: org, + year: lastYear, + }); + + // 5. Generate the Reports / Partners / Investors + await this.organizationReportService.generateNewReports({ + organization: org, + year: lastYear, + }); + } catch (err) { + this.logger.error(err); + Sentry.captureException(err, { + extra: { + organizationId: org.id, + year: lastYear, + }, + }); + } + } + } +} diff --git a/backend/src/modules/organization/services/organization-financial.service.ts b/backend/src/modules/organization/services/organization-financial.service.ts index 6512611fe..7a01019c8 100644 --- a/backend/src/modules/organization/services/organization-financial.service.ts +++ b/backend/src/modules/organization/services/organization-financial.service.ts @@ -4,7 +4,7 @@ import { OrganizationFinancialRepository } from '../repositories'; import { ORGANIZATION_ERRORS } from '../constants/errors.constants'; import { CompletionStatus } from '../enums/organization-financial-completion.enum'; import { FinancialType } from '../enums/organization-financial-type.enum'; -import { OrganizationFinancial } from '../entities'; +import { Organization, OrganizationFinancial } from '../entities'; import { AnafService, FinancialInformation, @@ -12,6 +12,8 @@ import { import { OnEvent } from '@nestjs/event-emitter'; import CUIChangedEvent from '../events/CUI-changed-event.class'; import { ORGANIZATION_EVENTS } from '../constants/events.constants'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import * as Sentry from '@sentry/node'; @Injectable() export class OrganizationFinancialService { @@ -20,7 +22,9 @@ export class OrganizationFinancialService { constructor( private readonly organizationFinancialRepository: OrganizationFinancialRepository, private readonly anafService: AnafService, - ) {} + ) { + // this.handleRegenerateFinancial({ organizationId: 170, cui: '29244879' }); + } @OnEvent(ORGANIZATION_EVENTS.CUI_CHANGED) async handleCuiChanged({ organizationId, newCUI }: CUIChangedEvent) { @@ -147,4 +151,48 @@ export class OrganizationFinancialService { totalIncome: income?.val_indicator, }; } + + public async generateNewReports({ + organization, + year, + }: { + organization: Organization; + year: number; + }): Promise { + if ( + organization.organizationFinancial.find( + (financial) => financial.year === year, + ) + ) { + // Avoid duplicating data + return; + } + + const financialFromAnaf = await this.getFinancialInformation( + organization.organizationGeneral.cui, + year, + ); + + // 3. Generate financial reports data + const newFinancialReport = this.generateFinancialReportsData( + year, + financialFromAnaf, + ); + + // 4. Save the new reports + try { + await Promise.all( + newFinancialReport.map((orgFinancial) => + this.organizationFinancialRepository.save({ + ...orgFinancial, + organizationId: organization.id, + }), + ), + ); + } catch (err) { + Sentry.captureException(err, { + extra: { organization, year }, + }); + } + } } diff --git a/backend/src/modules/organization/services/organization-report.service.ts b/backend/src/modules/organization/services/organization-report.service.ts index cd3d7bf93..2c546a03d 100644 --- a/backend/src/modules/organization/services/organization-report.service.ts +++ b/backend/src/modules/organization/services/organization-report.service.ts @@ -19,7 +19,7 @@ import { OrganizationReportRepository, PartnerRepository, } from '../repositories'; -import { OrganizationReport } from '../entities'; +import { Organization, OrganizationReport } from '../entities'; import { S3FileManagerService } from 'src/shared/services/s3-file-manager.service'; import { INVESTOR_LIST, @@ -28,6 +28,7 @@ import { } from '../constants/files.constants'; import { FILE_TYPE } from 'src/shared/enum/FileType.enum'; import { FILE_ERRORS } from 'src/shared/constants/file-errors.constants'; +import * as Sentry from '@sentry/node'; @Injectable() export class OrganizationReportService { @@ -228,4 +229,55 @@ export class OrganizationReportService { status: CompletionStatus.NOT_COMPLETED, }); } + + public async generateNewReports({ + organization, + year, + }: { + organization: Organization; + year: number; + }): Promise { + const organizationReport = organization.organizationReport; + + // Check if the given organizationId has already reports for the given year to avoid duplicating them + const hasReport = organizationReport.reports.find( + (report) => report.year === year, + ); + const hasPartners = organizationReport.partners.find( + (partner) => partner.year === year, + ); + const hasInvestors = organizationReport.investors.find( + (investor) => investor.year === year, + ); + + if (hasReport && hasPartners && hasInvestors) { + return; + } + + try { + await this.organizationReportRepository.save({ + ...organizationReport, + ...(!hasReport + ? { reports: [...organizationReport.reports, { year }] } + : {}), + ...(!hasPartners + ? { + partners: [...organizationReport.partners, { year }], + } + : {}), + ...(!hasInvestors + ? { + investors: [...organizationReport.investors, { year }], + } + : {}), + }); + } catch (err) { + Sentry.captureException(err, { + extra: { + organization, + year, + }, + }); + } + } } diff --git a/backend/src/shared/services/anaf.service.ts b/backend/src/shared/services/anaf.service.ts index 3ced11d67..7f40df01c 100644 --- a/backend/src/shared/services/anaf.service.ts +++ b/backend/src/shared/services/anaf.service.ts @@ -3,6 +3,7 @@ import { HttpService } from '@nestjs/axios'; import { ANAF_URL } from 'src/common/constants/anaf.constants'; import { firstValueFrom, lastValueFrom, of } from 'rxjs'; import { map, catchError } from 'rxjs/operators'; +import * as Sentry from '@sentry/node'; export interface FinancialInformation { numberOfEmployees: number; @@ -28,6 +29,12 @@ export class AnafService { map((res) => res.data.i), catchError((err) => { this.logger.error('ANAF error', err); + Sentry.captureException(err, { + extra: { + companyCUI, + year, + }, + }); return of(null); }), ) From 18836d4acfdc31eede1dc67a383be11c7ba1c94e Mon Sep 17 00:00:00 2001 From: Dragos-Paul Strat Date: Mon, 5 Aug 2024 15:53:19 +0300 Subject: [PATCH 02/57] feat: [517] add search and sort by alias instead of name --- .../organization/constants/organization-filter.config.ts | 2 +- .../organization/table-headers/OrganizationsTable.headers.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/modules/organization/constants/organization-filter.config.ts b/backend/src/modules/organization/constants/organization-filter.config.ts index f45069146..1fb4636fa 100644 --- a/backend/src/modules/organization/constants/organization-filter.config.ts +++ b/backend/src/modules/organization/constants/organization-filter.config.ts @@ -12,7 +12,7 @@ export const ORGANIZATION_FILTERS_CONFIG = { completionStatus: true, logo: true, }, - searchableColumns: ['name'], + searchableColumns: ['name', 'alias'], defaultSortBy: 'id', defaultOrderDirection: OrderDirection.ASC, relations: {}, diff --git a/frontend/src/pages/organization/table-headers/OrganizationsTable.headers.tsx b/frontend/src/pages/organization/table-headers/OrganizationsTable.headers.tsx index 0c21bde80..68357ef4b 100644 --- a/frontend/src/pages/organization/table-headers/OrganizationsTable.headers.tsx +++ b/frontend/src/pages/organization/table-headers/OrganizationsTable.headers.tsx @@ -24,7 +24,7 @@ const translations = { export const OrganizationsTableHeaders: TableColumn[] = [ { - id: 'name', + id: 'alias', name: , sortable: true, minWidth: '10rem', From 4ae6a1a7c543fff400e06ce89452c4cf59a755d2 Mon Sep 17 00:00:00 2001 From: Dragos-Paul Strat Date: Wed, 7 Aug 2024 17:39:36 +0300 Subject: [PATCH 03/57] feat: [476] wip edit application + label --- .../controllers/application.controller.ts | 18 --- .../src/shared/entities/application-labels.ts | 8 ++ .../src/assets/locales/ro/translation.json | 9 ++ .../components/AddApplicationConfig.ts | 27 ++++ .../apps-store/components/ApplicationForm.tsx | 9 +- .../components/ApplicationListTable.tsx | 122 +----------------- .../application/Application.queries.ts | 11 -- 7 files changed, 57 insertions(+), 147 deletions(-) create mode 100644 backend/src/shared/entities/application-labels.ts diff --git a/backend/src/modules/application/controllers/application.controller.ts b/backend/src/modules/application/controllers/application.controller.ts index 28fae5c19..9dd3843b3 100644 --- a/backend/src/modules/application/controllers/application.controller.ts +++ b/backend/src/modules/application/controllers/application.controller.ts @@ -108,24 +108,6 @@ export class ApplicationController { ); } - @Roles(Role.SUPER_ADMIN) - @ApiParam({ name: 'id', type: String }) - @Patch(':id/activate') - activate(@Param('id') id: number) { - return this.appService.update(id, { - status: ApplicationStatus.ACTIVE, - }); - } - - @Roles(Role.SUPER_ADMIN) - @ApiParam({ name: 'id', type: String }) - @Patch(':id/deactivate') - deactivate(@Param('id') id: number) { - return this.appService.update(id, { - status: ApplicationStatus.DISABLED, - }); - } - @Roles(Role.SUPER_ADMIN) @ApiParam({ name: 'id', type: String }) @ApiQuery({ type: () => ApplicationAccessFilterDto }) diff --git a/backend/src/shared/entities/application-labels.ts b/backend/src/shared/entities/application-labels.ts new file mode 100644 index 000000000..74b687b83 --- /dev/null +++ b/backend/src/shared/entities/application-labels.ts @@ -0,0 +1,8 @@ +import { BaseEntity } from 'src/common/base/base-entity.class'; +import { Column, Entity } from 'typeorm'; + +@Entity({ name: '_application-label' }) +export class ApplicationLabel extends BaseEntity { + @Column({ type: 'text', name: 'name' }) + name: string; +} diff --git a/frontend/src/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json index 0e215641a..467a974cb 100644 --- a/frontend/src/assets/locales/ro/translation.json +++ b/frontend/src/assets/locales/ro/translation.json @@ -1062,6 +1062,15 @@ "label": "Descriere scurtă", "helper": "Descrie aplicația în maxim 200 de caractere" }, + "status": { + "label": "Status aplicație", + "options": { + "active": "Activă (aplicația poate fi adăugată de organizații în profilul lor)", + "disabled": "Inactivă (aplicația nu poate fi adăugată de organizații în profilul lor)" + }, + "section_title": "Status aplicație", + "section_subtitle": "Statusul aplicației influențează disponibilitatea acesteia pentru organizații" + }, "description": { "required": "Descrierea extinsă este obligatorie", "max": "Descrierea extinsă poate avea maxim 7000 de caractere", diff --git a/frontend/src/pages/apps-store/components/AddApplicationConfig.ts b/frontend/src/pages/apps-store/components/AddApplicationConfig.ts index 7bbc8681c..949a6f6e9 100644 --- a/frontend/src/pages/apps-store/components/AddApplicationConfig.ts +++ b/frontend/src/pages/apps-store/components/AddApplicationConfig.ts @@ -3,6 +3,7 @@ import InputFieldHttpAddon from '../../../components/InputField/components/Input import { ApplicationTypeEnum, ApplicationTypeNaming } from '../constants/ApplicationType.enum'; import i18n from '../../../common/config/i18n'; import { ApplicationPullingType } from '../enums/application-pulling-type.enum'; +import { ApplicationStatus } from '../../../services/application/interfaces/Application.interface'; const translations = { name: { @@ -17,6 +18,9 @@ const translations = { label: i18n.t('appstore:config.type.label'), required: i18n.t('appstore:config.type.required'), }, + status: { + label: i18n.t('appstore:config.status.label'), + }, short: { required: i18n.t('appstore:config.short.required'), max: i18n.t('appstore:config.short.max'), @@ -139,6 +143,29 @@ export const AddAppConfig: Record = { }, ], }, + status: { + key: 'status', + label: translations.status.label, + rules: { + required: { + value: true, + message: translations.type.required, + }, + }, + helperText: '', + radioConfigs: [ + { + label: i18n.t('appstore:config.status.options.active'), + name: 'status', + value: ApplicationStatus.ACTIVE, + }, + { + label: i18n.t('appstore:config.status.options.disabled'), + name: 'status', + value: ApplicationStatus.DISABLED, + }, + ], + }, shortDescription: { key: 'shortDescription', rules: { diff --git a/frontend/src/pages/apps-store/components/ApplicationForm.tsx b/frontend/src/pages/apps-store/components/ApplicationForm.tsx index f73eb0cd7..f8ff9522c 100644 --- a/frontend/src/pages/apps-store/components/ApplicationForm.tsx +++ b/frontend/src/pages/apps-store/components/ApplicationForm.tsx @@ -20,10 +20,11 @@ import { CreateApplicationDto } from '../../../services/application/interfaces/A import { ApplicationTypeEnum } from '../constants/ApplicationType.enum'; import { AddAppConfig } from './AddApplicationConfig'; import RichText from '../../../components/RichText/RichText'; +import { ApplicationStatus } from '../../../services/application/interfaces/Application.interface'; interface ApplicationFormProps { control: Control; - errors: FieldErrorsImpl>; + errors: FieldErrorsImpl & { status?: ApplicationStatus }>; watch: UseFormWatch; file: File | null; setFile: (file: File) => void; @@ -264,6 +265,12 @@ const ApplicationForm = ({ )} {/* End Logo */} +
+ + {readonly && ( + + )} +
{fields.map((item, index) => { diff --git a/frontend/src/pages/apps-store/components/ApplicationListTable.tsx b/frontend/src/pages/apps-store/components/ApplicationListTable.tsx index 7fb9ffae6..6613c7c8d 100644 --- a/frontend/src/pages/apps-store/components/ApplicationListTable.tsx +++ b/frontend/src/pages/apps-store/components/ApplicationListTable.tsx @@ -5,17 +5,13 @@ import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { PaginationConfig } from '../../../common/config/pagination.config'; import { OrderDirection } from '../../../common/enums/sort-direction.enum'; -import { useErrorToast, useSuccessToast } from '../../../common/hooks/useToast'; -import ConfirmationModal from '../../../components/confim-removal-modal/ConfirmationModal'; +import { useErrorToast } from '../../../common/hooks/useToast'; import DataTableFilters from '../../../components/data-table-filters/DataTableFilters'; import DataTableComponent from '../../../components/data-table/DataTableComponent'; import PopoverMenu, { PopoverMenuRowType } from '../../../components/popover-menu/PopoverMenu'; import Select from '../../../components/Select/Select'; import { - useActivateApplication, useApplicationsQuery, - useDectivateApplication, - useRemoveApplication, } from '../../../services/application/Application.queries'; import { Application, @@ -34,13 +30,12 @@ const ApplicationListTable = () => { const [searchWord, setSearchWord] = useState(null); const [status, setStatus] = useState<{ status: ApplicationStatus; label: string } | null>(); const [type, setType] = useState<{ type: ApplicationTypeEnum; label: string } | null>(); - const [applicationToBeRemoved, setApplicationToBeRemoved] = useState(null); const navigate = useNavigate(); const { t } = useTranslation(['appstore', 'common']); - const { isLoading, error, refetch } = useApplicationsQuery( + const { isLoading, error } = useApplicationsQuery( rowsPerPage as number, page as number, orderByColumn as string, @@ -50,26 +45,8 @@ const ApplicationListTable = () => { type?.type, ); - const { - mutateAsync: activateApplication, - error: activateApplicationError, - isLoading: activateApplicationLoading, - } = useActivateApplication(); - - const { - mutateAsync: deactivateApplication, - error: deactivateApplicationError, - isLoading: deactivateApplicationLoading, - } = useDectivateApplication(); - const { applications } = useApplications(); - const { - mutateAsync: removeApplication, - error: removeApplicationError, - isLoading: removeApplicationLoading, - } = useRemoveApplication(); - useEffect(() => { if (applications?.meta) { setPage(applications.meta.currentPage); @@ -84,35 +61,9 @@ const ApplicationListTable = () => { useErrorToast(t('list.load_error')); } - if (activateApplicationError || deactivateApplicationError) { - useErrorToast(t('list.access_error')); - } - - if (removeApplicationError) { - useErrorToast(t('list.remove_error')); - } - }, [error, deactivateApplicationError, activateApplicationError, removeApplicationError]); + }, [error]); const buildUserActionColumn = (): TableColumn => { - const restrictedApplicationMenu = [ - { - name: t('list.view'), - icon: EyeIcon, - onClick: onView, - }, - { - name: t('list.activate'), - icon: ArrowPathIcon, - onClick: onActivateApplication, - type: PopoverMenuRowType.SUCCESS, - }, - { - name: t('list.remove'), - icon: TrashIcon, - onClick: onDeleteApplication, - type: PopoverMenuRowType.REMOVE, - }, - ]; const activeApplicationMenu = [ { @@ -121,12 +72,6 @@ const ApplicationListTable = () => { onClick: onView, type: PopoverMenuRowType.INFO, }, - { - name: t('list.restrict'), - icon: NoSymbolIcon, - onClick: onRestrictApplication, - type: PopoverMenuRowType.REMOVE, - }, ]; return { @@ -134,11 +79,7 @@ const ApplicationListTable = () => { cell: (row: Application) => ( ), width: '50px', @@ -179,52 +120,12 @@ const ApplicationListTable = () => { selected.type === ApplicationTypeEnum.ALL ? setType(null) : setType(selected); }; - const onActivateApplication = (row: Application) => { - activateApplication( - { applicationId: row.id.toString() }, - { - onSuccess: () => refetch(), - }, - ); - }; - - const onRestrictApplication = (row: Application) => { - deactivateApplication( - { applicationId: row.id.toString() }, - { - onSuccess: () => refetch(), - }, - ); - }; - const onResetFilters = () => { setStatus(null); setType(null); setSearchWord(null); }; - const onDeleteApplication = (row: Application) => { - setApplicationToBeRemoved(row.id); - }; - - const onConfirmDeleteApplication = () => { - if (applicationToBeRemoved) - removeApplication( - { applicationId: applicationToBeRemoved }, - { - onSuccess: () => { - useSuccessToast(t('list.remove_success')); - refetch(); - }, - onSettled: () => setApplicationToBeRemoved(null), - }, - ); - }; - - const onCancelRemoveApplication = () => { - setApplicationToBeRemoved(null); - }; - return (
{ columns={[...ApplicationtListTableHeaders, buildUserActionColumn()]} data={applications.items} loading={ - isLoading || - activateApplicationLoading || - deactivateApplicationLoading || - removeApplicationLoading + isLoading } pagination sortServer @@ -284,16 +182,6 @@ const ApplicationListTable = () => { onRowClicked={onView} />
- {applicationToBeRemoved && ( - - )}
); }; diff --git a/frontend/src/services/application/Application.queries.ts b/frontend/src/services/application/Application.queries.ts index 088dfa581..f0d4fc37c 100644 --- a/frontend/src/services/application/Application.queries.ts +++ b/frontend/src/services/application/Application.queries.ts @@ -143,17 +143,6 @@ export const useUpdateApplicationMutation = () => { ); }; -export const useActivateApplication = () => { - return useMutation(({ applicationId }: { applicationId: string }) => - activateApplication(applicationId), - ); -}; - -export const useDectivateApplication = () => { - return useMutation(({ applicationId }: { applicationId: string }) => - deactivateApplication(applicationId), - ); -}; export const useRestrictApplicationMutation = () => { return useMutation( From a8bf1e98ca2e60b1b53739920c95ecbba0fe280d Mon Sep 17 00:00:00 2001 From: Dragos-Paul Strat Date: Mon, 12 Aug 2024 10:43:37 +0300 Subject: [PATCH 04/57] feat: [476] wip application label --- .../1723119327053-ApplicationLabel.ts | 33 ++++++++++ .../application/dto/create-application.dto.ts | 4 ++ .../entities/application.entity.ts | 17 +++++- .../services/application.service.ts | 47 ++++++++++++++- .../controllers/nomenclatures.controller.ts | 5 ++ ...labels.ts => application-labels.entity.ts} | 0 .../shared/services/nomenclatures.service.ts | 13 ++++ backend/src/shared/shared.module.ts | 2 + frontend/src/common/helpers/format.helper.ts | 5 ++ .../interfaces/application-label.interface.ts | 3 + .../content-wrapper/ContentWrapper.tsx | 2 +- .../CreatableMultiSelect.tsx | 6 +- .../application/ApplicationWithOngList.tsx | 60 ++++++++++++++++++- .../components/AddApplicationConfig.ts | 10 ++++ .../apps-store/components/ApplicationForm.tsx | 41 +++++++++++-- .../apps-store/components/EditApplication.tsx | 7 +++ .../application/Application.queries.ts | 3 - .../application/Application.service.ts | 15 ++++- .../application/interfaces/Application.dto.ts | 1 + .../interfaces/Application.interface.ts | 2 + .../nomenclature/Nomenclature.queries.ts | 10 ++++ .../nomenclature/Nomenclatures.service.ts | 4 ++ .../nomenclature/nomenclature.selectors.ts | 2 + .../store/nomenclature/nomenclature.slice.ts | 5 ++ frontend/src/store/store.ts | 3 + 25 files changed, 281 insertions(+), 19 deletions(-) create mode 100644 backend/src/migrations/1723119327053-ApplicationLabel.ts rename backend/src/shared/entities/{application-labels.ts => application-labels.entity.ts} (100%) create mode 100644 frontend/src/common/interfaces/application-label.interface.ts diff --git a/backend/src/migrations/1723119327053-ApplicationLabel.ts b/backend/src/migrations/1723119327053-ApplicationLabel.ts new file mode 100644 index 000000000..4f3430333 --- /dev/null +++ b/backend/src/migrations/1723119327053-ApplicationLabel.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ApplicationLabel1723119327053 implements MigrationInterface { + name = 'ApplicationLabel1723119327053'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "_application-label" ("id" SERIAL NOT NULL, "deleted_on" TIMESTAMP WITH TIME ZONE, "created_on" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_on" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" text NOT NULL, CONSTRAINT "PK_c0aaf1127ad3beeaf0d3ad70096" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_e4a4e4b1582c4e665cff9be33e" ON "_application-label" ("created_on") `, + ); + await queryRunner.query( + `ALTER TABLE "application" ADD "application_label_id" integer`, + ); + await queryRunner.query( + `ALTER TABLE "application" ADD CONSTRAINT "FK_318029631a770782ba1c66721fd" FOREIGN KEY ("application_label_id") REFERENCES "_application-label"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "application" DROP CONSTRAINT "FK_318029631a770782ba1c66721fd"`, + ); + await queryRunner.query( + `ALTER TABLE "application" DROP COLUMN "application_label_id"`, + ); + await queryRunner.query( + `DROP INDEX "public"."IDX_e4a4e4b1582c4e665cff9be33e"`, + ); + await queryRunner.query(`DROP TABLE "_application-label"`); + } +} diff --git a/backend/src/modules/application/dto/create-application.dto.ts b/backend/src/modules/application/dto/create-application.dto.ts index 1726aacbb..78a303fe9 100644 --- a/backend/src/modules/application/dto/create-application.dto.ts +++ b/backend/src/modules/application/dto/create-application.dto.ts @@ -10,6 +10,7 @@ import { import { REGEX } from 'src/common/constants/patterns.constant'; import { ApplicationPullingType } from '../enums/application-pulling-type.enum'; import { ApplicationTypeEnum } from '../enums/ApplicationType.enum'; +import { ApplicationLabel } from 'src/shared/entities/application-labels.entity'; export class CreateApplicationDto { @IsString() @@ -56,4 +57,7 @@ export class CreateApplicationDto { @IsOptional() @Length(2, 100, { each: true }) steps?: string[]; + + @IsOptional() + applicationLabel: Partial; } diff --git a/backend/src/modules/application/entities/application.entity.ts b/backend/src/modules/application/entities/application.entity.ts index b88beb87e..05ebbc077 100644 --- a/backend/src/modules/application/entities/application.entity.ts +++ b/backend/src/modules/application/entities/application.entity.ts @@ -1,9 +1,17 @@ import { BaseEntity } from 'src/common/base/base-entity.class'; -import { Column, Entity, OneToMany } from 'typeorm'; +import { + Column, + Entity, + JoinColumn, + JoinTable, + ManyToOne, + OneToMany, +} from 'typeorm'; import { ApplicationPullingType } from '../enums/application-pulling-type.enum'; import { ApplicationStatus } from '../enums/application-status.enum'; import { ApplicationTypeEnum } from '../enums/ApplicationType.enum'; import { OngApplication } from './ong-application.entity'; +import { ApplicationLabel } from 'src/shared/entities/application-labels.entity'; @Entity() export class Application extends BaseEntity { @@ -60,4 +68,11 @@ export class Application extends BaseEntity { @OneToMany(() => OngApplication, (ongApp) => ongApp.application) ongApplications: OngApplication[]; + + @Column({ type: 'integer', name: 'application_label_id', nullable: true }) + applicationLabelId: number; + + @ManyToOne((type) => ApplicationLabel) + @JoinColumn({ name: 'application_label_id' }) + applicationLabel: ApplicationLabel; } diff --git a/backend/src/modules/application/services/application.service.ts b/backend/src/modules/application/services/application.service.ts index 901e8637f..9db9e131a 100644 --- a/backend/src/modules/application/services/application.service.ts +++ b/backend/src/modules/application/services/application.service.ts @@ -37,6 +37,8 @@ import { ApplicationTableViewRepository } from '../repositories/application-tabl import { ApplicationRepository } from '../repositories/application.repository'; import { OngApplicationRepository } from '../repositories/ong-application.repository'; import { UserOngApplicationRepository } from '../repositories/user-ong-application.repository'; +import { ApplicationLabel } from 'src/shared/entities/application-labels.entity'; +import { NomenclaturesService } from 'src/shared/services'; @Injectable() export class ApplicationService { @@ -49,6 +51,7 @@ export class ApplicationService { private readonly ongApplicationRepository: OngApplicationRepository, private readonly userOngApplicationRepository: UserOngApplicationRepository, private readonly applicationOngViewRepository: ApplicationOngViewRepository, + private readonly nomenclatureService: NomenclaturesService, ) {} public async create( @@ -250,6 +253,7 @@ export class ApplicationService { 'application.video_link as "videoLink"', 'application.pulling_type as "pullingType"', 'application.status as "applicationStatus"', + 'applicationLabel', ]) .leftJoin( 'ong_application', @@ -262,6 +266,11 @@ export class ApplicationService { 'userOngApp', 'userOngApp.ong_application_id = ongApp.id', ) + .leftJoin( + '_application-label', + 'applicationLabel', + 'applicationLabel.id = application.application_label_id', + ) .where('application.id = :applicationId', { applicationId }); // for employee add further filtersin by user id @@ -292,8 +301,19 @@ export class ApplicationService { ); } + const applicationLabel = { + id: applicationWithDetails.applicationLabel_id, + name: applicationWithDetails.applicationLabel_name, + }; + + delete applicationWithDetails.applicationLabel_id; + delete applicationWithDetails.applicationLabel_name; + delete applicationWithDetails.applicationLabel_created_on; + delete applicationWithDetails.applicationLabel_updated_on; + return { ...applicationWithDetails, + applicationLabel, logo, }; } @@ -346,7 +366,19 @@ export class ApplicationService { }; } - return this.applicationRepository.update({ id }, applicationPayload); + let applicationLabel = null; + if (applicationPayload.applicationLabel) { + applicationLabel = await this.saveAndGetApplicationLabel( + applicationPayload.applicationLabel, + ); + } + + console.log(applicationLabel); + + return this.applicationRepository.update( + { id }, + { ...applicationPayload, applicationLabel }, + ); } catch (error) { this.logger.error({ error: { error }, @@ -502,4 +534,17 @@ export class ApplicationService { return applicationCount; } + + private async saveAndGetApplicationLabel( + label: Partial, + ): Promise { + if (label.id) { + return label as ApplicationLabel; + } + + const newLabel = + await this.nomenclatureService.createApplicationLabel(label); + + return newLabel; + } } diff --git a/backend/src/shared/controllers/nomenclatures.controller.ts b/backend/src/shared/controllers/nomenclatures.controller.ts index 26060590d..c5adc2fd6 100644 --- a/backend/src/shared/controllers/nomenclatures.controller.ts +++ b/backend/src/shared/controllers/nomenclatures.controller.ts @@ -78,4 +78,9 @@ export class NomenclaturesController { getIssuers() { return this.nomenclaturesService.getIssuers({}); } + + @Get('application-labels') + getApplicationLabels() { + return this.nomenclaturesService.getApplicationLabels({}); + } } diff --git a/backend/src/shared/entities/application-labels.ts b/backend/src/shared/entities/application-labels.entity.ts similarity index 100% rename from backend/src/shared/entities/application-labels.ts rename to backend/src/shared/entities/application-labels.entity.ts diff --git a/backend/src/shared/services/nomenclatures.service.ts b/backend/src/shared/services/nomenclatures.service.ts index af2d37465..790afe0c9 100644 --- a/backend/src/shared/services/nomenclatures.service.ts +++ b/backend/src/shared/services/nomenclatures.service.ts @@ -17,6 +17,7 @@ import { Federation } from '../entities/federation.entity'; import { PracticeDomain } from 'src/modules/practice-program/entities/practice_domain.entity'; import { ServiceDomain } from 'src/modules/civic-center-service/entities/service-domain.entity'; import { Beneficiary } from 'src/modules/civic-center-service/entities/beneficiary.entity'; +import { ApplicationLabel } from '../entities/application-labels.entity'; @Injectable() export class NomenclaturesService { @@ -45,6 +46,8 @@ export class NomenclaturesService { private readonly beneficiaryRepository: Repository, @InjectRepository(Issuer) private readonly issuersRepository: Repository, + @InjectRepository(ApplicationLabel) + private readonly applicationLabelRepository: Repository, ) {} public getCity(conditions: FindOneOptions) { @@ -169,4 +172,14 @@ export class NomenclaturesService { public getIssuers(conditions: FindManyOptions) { return this.issuersRepository.find(conditions); } + + public getApplicationLabels(conditions: FindManyOptions) { + return this.applicationLabelRepository.find(conditions); + } + + public createApplicationLabel( + applicationLabel: Partial, + ): Promise { + return this.applicationLabelRepository.save(applicationLabel); + } } diff --git a/backend/src/shared/shared.module.ts b/backend/src/shared/shared.module.ts index dc13eab07..31086f825 100644 --- a/backend/src/shared/shared.module.ts +++ b/backend/src/shared/shared.module.ts @@ -20,6 +20,7 @@ import { FileManagerService } from './services/file-manager.service'; import { PracticeDomain } from 'src/modules/practice-program/entities/practice_domain.entity'; import { ServiceDomain } from 'src/modules/civic-center-service/entities/service-domain.entity'; import { Beneficiary } from 'src/modules/civic-center-service/entities/beneficiary.entity'; +import { ApplicationLabel } from './entities/application-labels.entity'; @Global() @Module({ @@ -37,6 +38,7 @@ import { Beneficiary } from 'src/modules/civic-center-service/entities/beneficia ServiceDomain, Beneficiary, Issuer, + ApplicationLabel, ]), HttpModule, ], diff --git a/frontend/src/common/helpers/format.helper.ts b/frontend/src/common/helpers/format.helper.ts index 818b93e60..d9f0e3027 100644 --- a/frontend/src/common/helpers/format.helper.ts +++ b/frontend/src/common/helpers/format.helper.ts @@ -70,6 +70,11 @@ export const mapSelectToSkill = ( ): { id?: number; name: string } => item?.__isNew__ ? { name: item.label } : { id: item.value, name: item.label }; +export const mapSelectToApplicationLabel = ( + item: ISelectData & { __isNew__?: boolean }, +): { id?: number; name: string } => + item?.__isNew__ ? { name: item.label } : { id: item.value, name: item.label }; + // Cities / Counties export const mapCitiesToSelect = (item: any): ISelectData => ({ value: item?.id, diff --git a/frontend/src/common/interfaces/application-label.interface.ts b/frontend/src/common/interfaces/application-label.interface.ts new file mode 100644 index 000000000..d3b372483 --- /dev/null +++ b/frontend/src/common/interfaces/application-label.interface.ts @@ -0,0 +1,3 @@ +import { BaseNomenclatureEntity } from './base-nomenclature-entity.interface'; + +export interface ApplicationLabel extends BaseNomenclatureEntity {} diff --git a/frontend/src/components/content-wrapper/ContentWrapper.tsx b/frontend/src/components/content-wrapper/ContentWrapper.tsx index 1ad0b486f..6eb629276 100644 --- a/frontend/src/components/content-wrapper/ContentWrapper.tsx +++ b/frontend/src/components/content-wrapper/ContentWrapper.tsx @@ -53,7 +53,7 @@ const ContentWrapper = ({ {fields.length > 0 && ( + + )} +
navigate('/')}> + NGO Hub +
+ + {!isAuthenticated && !isRestricted && !hideLogInButton && ( +
+ + {t('home')} +
)} -
navigate('/')}> - NGO Hub -
- - {!isAuthenticated && !isRestricted && !hideLogInButton && ( -
- - {t('home')} - - -
- )} - {isAuthenticated && !isRestricted && ( -
- -
- - - {profile?.name || ''} - - {/* Profile photo */} - - -
+ {isAuthenticated && !isRestricted && ( +
+ +
+ + + {profile?.name || ''} + + {/* Profile photo */} + + +
- - -
- - {({ active }) => ( - navigate('/account')} - > - - )} - - - {({ active }) => ( - - - )} - -
-
-
-
-
- )} -
- - + + +
+ + {({ active }) => ( + navigate('/account')} + > + + )} + + + {({ active }) => ( + + + )} + +
+
+
+ + + )} + + + + + + ); }; diff --git a/frontend/src/components/extended-statistics-card/ExtendedStatisticsCard.tsx b/frontend/src/components/extended-statistics-card/ExtendedStatisticsCard.tsx index 88e0d0fdb..80f218a32 100644 --- a/frontend/src/components/extended-statistics-card/ExtendedStatisticsCard.tsx +++ b/frontend/src/components/extended-statistics-card/ExtendedStatisticsCard.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { classNames } from '../../common/helpers/tailwind.helper'; +import { getYear } from 'date-fns'; interface ExtendedStatisticsCardInfo { icon: any; @@ -40,7 +41,14 @@ const ExetendedStatisticsCard = ({ stat }: { stat: ExtendedStatisticsCardInfo }) {t(row.title.toString())} - {t(row.subtitle)} + {t(row.subtitle, { + value: + row.subtitle === 'statistics.next_update' + ? getYear(new Date()) + : row.subtitle === 'statistics.next_year_update' + ? getYear(new Date()) + 1 + : undefined, + })} ))} diff --git a/frontend/src/components/status-badge/StatusBadge.tsx b/frontend/src/components/status-badge/StatusBadge.tsx index 79fcfc49a..b63c2a437 100644 --- a/frontend/src/components/status-badge/StatusBadge.tsx +++ b/frontend/src/components/status-badge/StatusBadge.tsx @@ -1,5 +1,7 @@ import React from 'react'; import { classNames } from '../../common/helpers/tailwind.helper'; +import { Tooltip } from 'react-tooltip'; +import colors from 'tailwindcss/colors'; export enum BadgeStatus { SUCCESS = 'success', @@ -10,32 +12,61 @@ export enum BadgeStatus { interface StatusBadgeProps { status: BadgeStatus; value: string; + tooltip?: boolean; + tooltipContent?: string; } -const StatusBadge = ({ status, value }: StatusBadgeProps) => { +const StatusBadge = ({ status, value, tooltip, tooltipContent = '' }: StatusBadgeProps) => { return ( - - - - - {value} - + + + + {value} + + {tooltip && ( + + )} + ); }; diff --git a/frontend/src/components/warning-banner/WarningBanner.tsx b/frontend/src/components/warning-banner/WarningBanner.tsx new file mode 100644 index 000000000..39664a024 --- /dev/null +++ b/frontend/src/components/warning-banner/WarningBanner.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { ExclamationTriangleIcon } from '@heroicons/react/24/solid'; + +interface WarningBannerProps { + text: string; + actionText: string; +} +const WarningBanner = ({ text, actionText }: WarningBannerProps) => { + return ( +
+
+ +

+ {text}{' '} + + {actionText} + +

+
+ ); +}; + +export default WarningBanner; diff --git a/frontend/src/pages/dashboard/constants/DashboardStatistics.constants.ts b/frontend/src/pages/dashboard/constants/DashboardStatistics.constants.ts index bf734aa63..1be2c65d8 100644 --- a/frontend/src/pages/dashboard/constants/DashboardStatistics.constants.ts +++ b/frontend/src/pages/dashboard/constants/DashboardStatistics.constants.ts @@ -1,5 +1,4 @@ import { - CheckCircleIcon, ClockIcon, RectangleStackIcon, SunIcon, @@ -7,6 +6,7 @@ import { UsersIcon, SquaresPlusIcon, } from '@heroicons/react/24/solid'; +import { ExclamationTriangleIcon, CheckCircleIcon } from '@heroicons/react/24/outline'; import { formatDate } from '../../../common/helpers/format.helper'; interface PartialSimpleDashboardStatistics { @@ -63,14 +63,14 @@ export const AdminDashboardSimpleStatisticsMapping: Record< export const AdminEmployeeDashboardExtendedStatisticsMapping = { isOrganizationFinancialReportsUpdated: (isUpdated: boolean) => ({ - icon: CheckCircleIcon, + icon: isUpdated ? CheckCircleIcon : ExclamationTriangleIcon, alert: !isUpdated, info: [ { title: isUpdated - ? 'TODO: De mutat in banner - Rapoartele financiare sunt actualizate' - : 'TODO: De mutat in banner - Rapoartele financiare nu sunt actualizate', - subtitle: 'statistics.next_update', + ? 'Informațiile financiare sunt actualizate' + : 'Informațiile financiare nu sunt actualizate', + subtitle: isUpdated ? 'statistics.next_year_update' : 'statistics.next_update', }, ], button: { @@ -79,14 +79,14 @@ export const AdminEmployeeDashboardExtendedStatisticsMapping = { }, }), isOrganizationReportsPartnersInvestorsUpdated: (isUpdated: boolean) => ({ - icon: CheckCircleIcon, + icon: isUpdated ? CheckCircleIcon : ExclamationTriangleIcon, alert: !isUpdated, info: [ { title: isUpdated - ? 'TODO: De mutat in banner - Rapoartele sunt actualizate' - : 'TODO: De mutat in banner - Rapoartele nu sunt actualizate', - subtitle: 'statistics.next_update', + ? 'Informațiile din secțiunea “ONG-ul în numere” sunt actualizate' + : 'Informațiile din secțiunea “ONG-ul în numere” nu sunt actualizate', + subtitle: isUpdated ? 'statistics.next_year_update' : 'statistics.next_update', }, ], button: { @@ -133,14 +133,14 @@ export const AdminEmployeeDashboardExtendedStatisticsMapping = { export const SuperAdminOverviewExtendedStatisticsMapping = { isOrganizationFinancialReportsUpdated: (isUpdated: boolean, organizationId?: number) => ({ - icon: CheckCircleIcon, + icon: isUpdated ? CheckCircleIcon : ExclamationTriangleIcon, alert: !isUpdated, info: [ { title: isUpdated - ? 'TODO: De mutat in banner - Rapoartele sunt actualizate' - : 'TODO: De mutat in banner - Rapoartele nu sunt actualizate', - subtitle: 'statistics.next_update', + ? 'Informațiile financiare sunt actualizate' + : 'Informațiile financiare nu sunt actualizate', + subtitle: isUpdated ? 'statistics.next_year_update' : 'statistics.next_update', }, ], button: { @@ -149,14 +149,14 @@ export const SuperAdminOverviewExtendedStatisticsMapping = { }, }), isOrganizationReportsPartnersInvestorsUpdated: (isUpdated: boolean, organizationId?: number) => ({ - icon: CheckCircleIcon, + icon: ExclamationTriangleIcon, alert: !isUpdated, info: [ { title: isUpdated - ? 'TODO: De mutat in banner - Rapoartele sunt actualizate' - : 'TODO: De mutat in banner - Rapoartele nu sunt actualizate', - subtitle: 'statistics.next_update', + ? 'Informațiile din secțiunea “ONG-ul în numere” sunt actualizate' + : 'Informațiile din secțiunea “ONG-ul în numere” nu sunt actualizate', + subtitle: isUpdated ? 'statistics.next_year_update' : 'statistics.next_update', }, ], button: { diff --git a/frontend/src/pages/organization/components/OrganizationFinancial/OrganizationFinancialTableHeaders.tsx b/frontend/src/pages/organization/components/OrganizationFinancial/OrganizationFinancialTableHeaders.tsx index 27631eeb2..2386be5a1 100644 --- a/frontend/src/pages/organization/components/OrganizationFinancial/OrganizationFinancialTableHeaders.tsx +++ b/frontend/src/pages/organization/components/OrganizationFinancial/OrganizationFinancialTableHeaders.tsx @@ -26,15 +26,39 @@ const translations = { const mapReportStatusToTextAndBadge = (status: OrganizationFinancialReportStatus) => { switch (status) { case OrganizationFinancialReportStatus.COMPLETED: - return { translation: translations.completed, badge: BadgeStatus.SUCCESS }; + return { + translation: translations.completed, + badge: BadgeStatus.SUCCESS, + tooltipContent: + 'financial reports exist, admin filled in and it checks out against ANAF information', + }; case OrganizationFinancialReportStatus.INVALID: - return { translation: translations.invalid, badge: BadgeStatus.ERROR }; + return { + translation: translations.invalid, + badge: BadgeStatus.ERROR, + tooltipContent: + 'financial reports exist, admin filled in but it does not check out against ANAF information', + }; case OrganizationFinancialReportStatus.NOT_COMPLETED: - return { translation: translations.not_completed, badge: BadgeStatus.WARNING }; + return { + translation: translations.not_completed, + badge: BadgeStatus.WARNING, + tooltipContent: 'financial reports exist, but no data has been added', + }; case OrganizationFinancialReportStatus.PENDING: - return { translation: translations.pending, badge: BadgeStatus.WARNING }; + return { + translation: translations.pending, + badge: BadgeStatus.WARNING, + tooltipContent: + 'financial reports exist, admin filled in some information, but ANAF information is not yet ready', + }; default: - return { translation: 'Error', badge: BadgeStatus.ERROR }; + return { + translation: 'Error', + badge: BadgeStatus.ERROR, + tooltipContent: + 'Error error error anaf error Error error error anaf error Error error error anaf error ', + }; } }; @@ -88,6 +112,8 @@ export const OrganizationFinancialTableHeaders: TableColumn ), sortable: true, diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 75a4e663c..9d52c0a1b 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -37,6 +37,19 @@ module.exports = { 800: '#F9A825', 900: '#F57F17', }, + amber: { + 50: '#fffbeb', + 100: '#fef3c7', + 200: '#fde68a', + 300: '#fcd34d', + 400: '#fbbf24', + 500: '#f59e0b', + 600: '#d97706', + 700: '#b45309', + 800: '#92400e', + 900: '#78350f', + 950: '#451a03', + }, blue: { 50: '#EEF2FF', 100: '#E0E7FF', From f7ad490a3925c7b1e87c29f2dc46507e77e949dd Mon Sep 17 00:00:00 2001 From: luciatugui Date: Wed, 21 Aug 2024 17:15:07 +0300 Subject: [PATCH 21/57] fix [#600]: broken safari landing page --- frontend/src/pages/login/Login.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/login/Login.tsx b/frontend/src/pages/login/Login.tsx index 80e537037..31b1676cd 100644 --- a/frontend/src/pages/login/Login.tsx +++ b/frontend/src/pages/login/Login.tsx @@ -67,7 +67,7 @@ const Login = () => {
{t('description')}
-
+
{t('about.title')}
{t('about.description')}
@@ -80,7 +80,7 @@ const Login = () => { {t('about.start_form')}
-
+
From 2ee45515e9fa152057f8a4ec9ac7045de621cf8f Mon Sep 17 00:00:00 2001 From: Dragos-Paul Strat Date: Wed, 21 Aug 2024 17:44:36 +0300 Subject: [PATCH 22/57] wip 495 comm fixes --- .../src/assets/locales/ro/translation.json | 6 +++-- .../CreatableMultiSelect.tsx | 3 ++- frontend/src/index.css | 21 +++++++++------ .../components/AddApplicationConfig.ts | 13 ++++++--- .../apps-store/components/ApplicationForm.tsx | 14 ++++++---- .../components/ApplicationListTable.tsx | 27 +------------------ 6 files changed, 38 insertions(+), 46 deletions(-) diff --git a/frontend/src/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json index d25ead90c..e84035384 100644 --- a/frontend/src/assets/locales/ro/translation.json +++ b/frontend/src/assets/locales/ro/translation.json @@ -48,7 +48,7 @@ "reset": "Resetează filtre", "show": "Filtre" }, - "unavailable": "În curând", + "unavailable": "Temporar indisponibilă", "to_be_removed": "De eliminat", "decimal": "Valori cu decimale nu sunt permise.", "unknown_error": "A apărut o eroare necunoscută", @@ -1065,6 +1065,7 @@ }, "status": { "label": "Status aplicație", + "required": "Statusul aplicației este obligatoriu", "options": { "active": "Activă (aplicația poate fi adăugată de organizații în profilul lor)", "disabled": "Inactivă (aplicația nu poate fi adăugată de organizații în profilul lor)" @@ -1111,7 +1112,8 @@ "application_label": { "label": "Eticheta pentru aplicație (eticheta apare în meniul Toate aplicațiile)", "helper": "Adaugă o etichetă deja existentă sau creează una nouă", - "maxLength": "Eticheta poate avea maxim 30 de caractere" + "maxLength": "Eticheta poate avea maxim 30 de caractere", + "minLength": "Eticheta poate avea minimum 2 de caractere" } }, "request_modal": { diff --git a/frontend/src/components/creatable-multi-select/CreatableMultiSelect.tsx b/frontend/src/components/creatable-multi-select/CreatableMultiSelect.tsx index b65e98478..0d98a9192 100644 --- a/frontend/src/components/creatable-multi-select/CreatableMultiSelect.tsx +++ b/frontend/src/components/creatable-multi-select/CreatableMultiSelect.tsx @@ -71,9 +71,10 @@ const CreatableMultiSelect = ({ onChange={onChange} value={value} options={options} + isClearable id={id} formatCreateLabel={(text) => `${i18n.t('common:add_option')}: ${text}`} - isValidNewOption={validation ? validation : () => true} + isValidNewOption={(validation ? validation : () => true)} /> )} {!error && !readonly && helperText && ( diff --git a/frontend/src/index.css b/frontend/src/index.css index cd01e5a8d..6387cbc9c 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -176,21 +176,26 @@ transform: rotate(-45deg); position: absolute; background-color: #374159; - padding-top: 0.5rem; - padding-left: 3.5rem; - padding-bottom: 0.5rem; - padding-right: 4.5rem; + padding: 0.5rem 3.1rem; color: white; + width: 14rem; /* Adjust the width to fit text nicely */ + text-align: center; overflow: hidden; - width: 18rem; - margin-left: -0.5rem; + left: -3.5rem; /* Adjusted positioning */ + top: 2rem; /* Adjust as necessary */ + + @media screen and (max-width: 768px) { + width: 13rem; + } } .ribbon p { margin: 0; - white-space: normal; - overflow-wrap: break-word; + white-space: normal; /* Allow text to wrap */ + overflow-wrap: break-word; /* Handle long words */ + word-wrap: break-word; /* Ensure compatibility with older browsers */ text-align: center; + line-height: 1; /* Adjust line height to maintain readability */ } .richtext_html ul { diff --git a/frontend/src/pages/apps-store/components/AddApplicationConfig.ts b/frontend/src/pages/apps-store/components/AddApplicationConfig.ts index 74bae085b..14e17e5e1 100644 --- a/frontend/src/pages/apps-store/components/AddApplicationConfig.ts +++ b/frontend/src/pages/apps-store/components/AddApplicationConfig.ts @@ -20,6 +20,7 @@ const translations = { }, status: { label: i18n.t('appstore:config.status.label'), + required: i18n.t('appstore:config.status.required'), }, short: { required: i18n.t('appstore:config.short.required'), @@ -80,11 +81,11 @@ export const PullingTypeOptions = [ export function isHtmlContentEmpty(html: string): boolean { // Remove all HTML tags - const stripped = html.replace(/<[^>]*>/g, ''); + const stripped = html?.replace(/<[^>]*>/g, ''); // Remove whitespace - const trimmed = stripped.trim(); + const trimmed = stripped?.trim(); - return trimmed.length === 0; + return trimmed?.length === 0; } export const AddAppConfig: Record = { @@ -149,7 +150,7 @@ export const AddAppConfig: Record = { rules: { required: { value: true, - message: translations.type.required, + message: translations.status.required, }, }, helperText: '', @@ -173,6 +174,10 @@ export const AddAppConfig: Record = { value: 30, message: translations.short.max, }, + minLength: { + value: 2, + message: translations.short.max, + }, }, config: { type: 'text', diff --git a/frontend/src/pages/apps-store/components/ApplicationForm.tsx b/frontend/src/pages/apps-store/components/ApplicationForm.tsx index 9fb0140fd..9091a03cd 100644 --- a/frontend/src/pages/apps-store/components/ApplicationForm.tsx +++ b/frontend/src/pages/apps-store/components/ApplicationForm.tsx @@ -73,11 +73,15 @@ const ApplicationForm = ({ if (inputValue.length > 30 && !(errors as Record)[AddAppConfig.applicationLabel.key]) { control.setError(AddAppConfig.applicationLabel.key, { type: 'maxLength', message: t('config.application_label.maxLength') }); return false; - } else if (inputValue.length > 30) { - return false; - } else if (inputValue.length < 30 && clearErrors && (errors as Record)[AddAppConfig.applicationLabel.key]) { + } else if (inputValue.length > 2 && inputValue.length < 30 && clearErrors && (errors as Record)[AddAppConfig.applicationLabel.key]) { clearErrors(AddAppConfig.applicationLabel.key); + return true + } else if (inputValue.length > 0 && inputValue.length < 3 && !(errors as Record)[AddAppConfig.applicationLabel.key]) { + control.setError(AddAppConfig.applicationLabel.key, { type: 'minLength', message: t('config.application_label.minLength') }); + return false } + + console.log('inputValue', inputValue); return true; } @@ -286,7 +290,7 @@ const ApplicationForm = ({ )} {/* End Logo */} - {readonly &&
+
-
} +
{fields.map((item, index) => { diff --git a/frontend/src/pages/apps-store/components/ApplicationListTable.tsx b/frontend/src/pages/apps-store/components/ApplicationListTable.tsx index 6613c7c8d..465a670b7 100644 --- a/frontend/src/pages/apps-store/components/ApplicationListTable.tsx +++ b/frontend/src/pages/apps-store/components/ApplicationListTable.tsx @@ -8,13 +8,11 @@ import { OrderDirection } from '../../../common/enums/sort-direction.enum'; import { useErrorToast } from '../../../common/hooks/useToast'; import DataTableFilters from '../../../components/data-table-filters/DataTableFilters'; import DataTableComponent from '../../../components/data-table/DataTableComponent'; -import PopoverMenu, { PopoverMenuRowType } from '../../../components/popover-menu/PopoverMenu'; import Select from '../../../components/Select/Select'; import { useApplicationsQuery, } from '../../../services/application/Application.queries'; import { - Application, ApplicationStatus, } from '../../../services/application/interfaces/Application.interface'; import { useApplications } from '../../../store/selectors'; @@ -63,29 +61,6 @@ const ApplicationListTable = () => { }, [error]); - const buildUserActionColumn = (): TableColumn => { - - const activeApplicationMenu = [ - { - name: t('list.view'), - icon: EyeIcon, - onClick: onView, - type: PopoverMenuRowType.INFO, - }, - ]; - - return { - name: '', - cell: (row: Application) => ( - - ), - width: '50px', - allowOverflow: true, - }; - }; const onRowsPerPageChange = (rows: number) => { setRowsPerPage(rows); @@ -165,7 +140,7 @@ const ApplicationListTable = () => {

Date: Thu, 22 Aug 2024 10:53:59 +0300 Subject: [PATCH 23/57] feat: [438] Fetch ANAF API for organization with financial reports missing ANAF data --- .../organization/services/crons.service.ts | 13 ++ .../organization-financial.service.ts | 149 ++++++++++++++++-- .../statistics/services/statistics.service.ts | 1 - 3 files changed, 150 insertions(+), 13 deletions(-) diff --git a/backend/src/modules/organization/services/crons.service.ts b/backend/src/modules/organization/services/crons.service.ts index 52aa7cfae..e0dd9bc35 100644 --- a/backend/src/modules/organization/services/crons.service.ts +++ b/backend/src/modules/organization/services/crons.service.ts @@ -119,4 +119,17 @@ export class OrganizationCronsService { }); } } + + /** + * At 07:00 (Server Time) every Monday in every month from June through December. + */ + @Cron('0 7 * 6-12 1') + async fetchANAFDataForFinancialReports() { + try { + await this.organizationFinancialService.refetchANAFDataForFinancialReports(); + } catch (err) { + Sentry.captureMessage('fetchANAFDataForFinancialReports failed'); + Sentry.captureException(err); + } + } } diff --git a/backend/src/modules/organization/services/organization-financial.service.ts b/backend/src/modules/organization/services/organization-financial.service.ts index f039c217b..35dbb32cd 100644 --- a/backend/src/modules/organization/services/organization-financial.service.ts +++ b/backend/src/modules/organization/services/organization-financial.service.ts @@ -1,6 +1,9 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { UpdateOrganizationFinancialDto } from '../dto/update-organization-financial.dto'; -import { OrganizationFinancialRepository } from '../repositories'; +import { + OrganizationFinancialRepository, + OrganizationRepository, +} from '../repositories'; import { ORGANIZATION_ERRORS } from '../constants/errors.constants'; import { CompletionStatus, @@ -24,9 +27,11 @@ export class OrganizationFinancialService { constructor( private readonly organizationFinancialRepository: OrganizationFinancialRepository, + private readonly organizationRepository: OrganizationRepository, private readonly anafService: AnafService, ) { // this.handleRegenerateFinancial({ organizationId: 170, cui: '29244879' }); + // this.refetchANAFDataForFinancialReports(); } // TODO: Deprecated, we don't allow changing the CUI anymore, so this is obsolete. To be discussed and deleted @@ -101,17 +106,11 @@ export class OrganizationFinancialService { let reportStatus: OrganizationFinancialReportStatus; // BR: Look into OrganizationFinancialReportStatus (ENUM) to understand the business logic behind the statuses - if (organizationFinancial.synched_anaf) { - if (organizationFinancial.total === totals) { - reportStatus = OrganizationFinancialReportStatus.COMPLETED; - } else { - reportStatus = OrganizationFinancialReportStatus.INVALID; - } - } else if (totals !== 0) { - reportStatus = OrganizationFinancialReportStatus.PENDING; - } else { - reportStatus = OrganizationFinancialReportStatus.NOT_COMPLETED; - } + reportStatus = this.determineReportStatus( + totals, + organizationFinancial.total, + organizationFinancial.synched_anaf, + ); return this.organizationFinancialRepository.save({ ...organizationFinancial, @@ -147,6 +146,24 @@ export class OrganizationFinancialService { ]; } + private determineReportStatus( + addedByOrganizationTotal: number, + anafTotal: number, + isSynced: boolean, + ) { + if (isSynced) { + if (anafTotal === addedByOrganizationTotal) { + return OrganizationFinancialReportStatus.COMPLETED; + } else { + return OrganizationFinancialReportStatus.INVALID; + } + } else if (addedByOrganizationTotal !== 0) { + return OrganizationFinancialReportStatus.PENDING; + } else { + return OrganizationFinancialReportStatus.NOT_COMPLETED; + } + } + public async getFinancialInformationFromANAF( cui: string, year: number, @@ -233,4 +250,112 @@ export class OrganizationFinancialService { return count; } + + async refetchANAFDataForFinancialReports() { + // 1. Find all organizations with missing ANAF data in the Financial Reports + type OrganizationsWithMissingANAFData = { + id: number; + organizationFinancial: OrganizationFinancial[]; + organizationGeneral: { cui: string }; + }; + + const data: OrganizationsWithMissingANAFData[] = + await this.organizationRepository.getMany({ + relations: { + organizationFinancial: true, + organizationGeneral: true, + }, + where: { + organizationFinancial: { + synched_anaf: false, + }, + }, + select: { + id: true, + organizationGeneral: { + cui: true, + }, + organizationFinancial: true, + }, + }); + + for (let org of data) { + try { + // Find all years for which we should call ANAF services + type YearsFinancial = { + [year: string]: { + Income: { id: number; existingTotal: number }; + Expense: { id: number; existingTotal: number }; + }; + }; + const years: YearsFinancial = org.organizationFinancial.reduce( + (acc, curr) => { + if (!acc[curr.year]) { + acc[curr.year] = {}; + } + + const existingData = curr.data as Object; + let existingTotal = null; + + if (curr.data) { + existingTotal = Object.keys(existingData).reduce( + (acc, curr) => acc + +existingData[curr], + 0, + ); + } + + acc[curr.year][curr.type] = { id: curr.id, existingTotal }; + + return acc; + }, + {}, + ); + + // Iterate over all years and call ANAF + for (let year of Object.keys(years)) { + // 2. Fetch data from ANAF for the given CUI and YEAR + const anafData = await this.getFinancialInformationFromANAF( + org.organizationGeneral.cui, + +year, + ); + + if (anafData) { + // We have the data, upadate the reports. First "Income" + if (years[year].Income) + await this.organizationFinancialRepository.updateOne({ + id: years[year].Income.id, + total: anafData.totalIncome, + numberOfEmployees: anafData.numberOfEmployees, + synched_anaf: true, + reportStatus: this.determineReportStatus( + years[year].Income.existingTotal, + anafData.totalIncome, + true, + ), + }); + + // Second: "Expense" + if (years[year].Expense) + await this.organizationFinancialRepository.updateOne({ + id: years[year].Expense.id, + total: anafData.totalExpense, + numberOfEmployees: anafData.numberOfEmployees, + synched_anaf: true, + reportStatus: this.determineReportStatus( + years[year].Expense.existingTotal, + anafData.totalExpense, + true, + ), + }); + } + } + } catch (err) { + Sentry.captureException(err, { + extra: { + organization: org.id, + }, + }); + } + } + } } diff --git a/backend/src/modules/statistics/services/statistics.service.ts b/backend/src/modules/statistics/services/statistics.service.ts index 5f83551b6..d639e4f98 100644 --- a/backend/src/modules/statistics/services/statistics.service.ts +++ b/backend/src/modules/statistics/services/statistics.service.ts @@ -274,7 +274,6 @@ export class StatisticsService { } } - // TODO: Cata - we can transform this in a view public async getOrganizationStatistics( organizationId: number, role?: Role, From e74c6b769e29c7d0d3185cc7f17ba9ef27e09549 Mon Sep 17 00:00:00 2001 From: luciatugui Date: Thu, 22 Aug 2024 11:39:20 +0300 Subject: [PATCH 24/57] style: dashboard cards & translations --- .../src/assets/locales/en/translation.json | 24 +++++++++++++------ .../src/assets/locales/ro/translation.json | 17 ++++++++++++- .../ExtendedStatisticsCard.tsx | 5 ++-- frontend/src/pages/dashboard/Dashboard.tsx | 23 ++++++++++-------- .../DashboardStatistics.constants.ts | 16 ++++++------- .../OrganizationFinancialTableHeaders.tsx | 21 +++++++++------- 6 files changed, 68 insertions(+), 38 deletions(-) diff --git a/frontend/src/assets/locales/en/translation.json b/frontend/src/assets/locales/en/translation.json index ab772bb6c..92ae7a712 100644 --- a/frontend/src/assets/locales/en/translation.json +++ b/frontend/src/assets/locales/en/translation.json @@ -499,19 +499,16 @@ "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", @@ -533,12 +530,10 @@ "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": "" @@ -675,6 +670,13 @@ "info": "The total income resulting from financial operations, for example the interest you get on deposits or exchange rate differences." } }, + "tooltip": { + "completed": "All information has been completed and matches that of ANAF", + "not_completed": "Information has not been completed yet. Please enter your financial information to avoid account suspension", + "invalid": "The information entered does not match that of ANAF. Please review and correct any errors", + "pending": "The information has been partially or fully completed and is being checked against the information from ANAF", + "error": "Error getting status" + }, "expense": { "net": { "label": "Net salaries (human resources)", @@ -1249,7 +1251,7 @@ "view_active_apps": "View active apps", "active_users": "Users in the organisation", "handle_users": "Manage users", - "view_data": "View data", + "view_data": "View information", "financial_reports_are_uptodate": "The Financial Reports are up-to-date", "financial_reports_are_outdated": "The Financial Reports needs to be updated", "error": "Data retrieval error", @@ -1266,6 +1268,14 @@ "yearly": "Since forever", "monthly": "Lunar", "daily": "Daily" + }, + "financial_reports": { + "updated": "Financial information is updated", + "not_updated": "Financial information is not updated" + }, + "organization_reports": { + "updated": "The information in the “NGO in numbers“ section is updated", + "not_updated": "The information in the “NGO in numbers“ section is not updated" } }, "activity_title": "Organisation active in the NGO Hub in", @@ -1608,4 +1618,4 @@ "date_added": "Date of feedback" } } -} +} \ 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 22d5f6ad4..50de52832 100644 --- a/frontend/src/assets/locales/ro/translation.json +++ b/frontend/src/assets/locales/ro/translation.json @@ -719,6 +719,13 @@ "info": "Totalul veniturilor care au rezultat in urma unor operațiuni financiare, de exemplu dobânda pe care o obții la depozite sau diferențele de schimb valutar." } }, + "tooltip": { + "completed": "Toate informațiile au fost completate și se potrivesc cu cele de la ANAF", + "not_completed": "Informațiile nu au fost completate încă. Te rugăm să introduci informațiile financiare pentru a evita suspendarea contului", + "invalid": "Informațiile introduse nu se potrivesc cu cele de la ANAF. Te rugăm să le revizuiești și să corectezi eventualele erori", + "pending": "Informațiile au fost completate parțial sau integral și sunt în curs de verificare cu informațiile de la ANAF", + "error": "Eroare la obținerea statusului" + }, "expense": { "net": { "label": "Salarii nete (resurse umane)", @@ -1309,7 +1316,7 @@ "view_active_apps": "Vezi aplicațiile active", "active_users": "Utilizatori în organizație", "handle_users": "Gestionează utilizatori", - "view_data": "Vizualizează datele", + "view_data": "Vizualizează informațiile", "financial_reports_are_uptodate": "Rapoartele financiare sunt actualizate", "financial_reports_are_outdated": "Rapoartele financiare nu sunt actualizate", "error": "Eroare la preluarea datelor", @@ -1326,6 +1333,14 @@ "yearly": "Dintotdeauna", "monthly": "Lunar", "daily": "Zilnic" + }, + "financial_reports": { + "updated": "Informațiile financiare sunt actualizate", + "not_updated": "Informațiile financiare nu sunt actualizate" + }, + "organization_reports": { + "updated": "Informațiile din secțiunea “ONG-ul în numere” sunt actualizate", + "not_updated": "Informațiile din secțiunea “ONG-ul în numere” nu sunt actualizate" } }, "activity_title": "Organizație activă în NGO Hub din", diff --git a/frontend/src/components/extended-statistics-card/ExtendedStatisticsCard.tsx b/frontend/src/components/extended-statistics-card/ExtendedStatisticsCard.tsx index 80f218a32..b1855052d 100644 --- a/frontend/src/components/extended-statistics-card/ExtendedStatisticsCard.tsx +++ b/frontend/src/components/extended-statistics-card/ExtendedStatisticsCard.tsx @@ -19,7 +19,6 @@ interface ExtendedStatisticsCardInfo { const ExetendedStatisticsCard = ({ stat }: { stat: ExtendedStatisticsCardInfo }) => { const navigate = useNavigate(); - const { t } = useTranslation(['dashboard']); const navigateToLink = () => { @@ -31,8 +30,8 @@ const ExetendedStatisticsCard = ({ stat }: { stat: ExtendedStatisticsCardInfo }) return (
-
- +
+
{stat.info.map((row, key) => ( diff --git a/frontend/src/pages/dashboard/Dashboard.tsx b/frontend/src/pages/dashboard/Dashboard.tsx index 7c8325d99..ae5a6185e 100644 --- a/frontend/src/pages/dashboard/Dashboard.tsx +++ b/frontend/src/pages/dashboard/Dashboard.tsx @@ -35,16 +35,19 @@ const Dashboard = () => {
- - +
+ + +
+
{ @@ -29,35 +36,31 @@ const mapReportStatusToTextAndBadge = (status: OrganizationFinancialReportStatus return { translation: translations.completed, badge: BadgeStatus.SUCCESS, - tooltipContent: - 'financial reports exist, admin filled in and it checks out against ANAF information', + tooltipContent: translations.tooltip.completed, }; case OrganizationFinancialReportStatus.INVALID: return { translation: translations.invalid, badge: BadgeStatus.ERROR, - tooltipContent: - 'financial reports exist, admin filled in but it does not check out against ANAF information', + tooltipContent: translations.tooltip.invalid, }; case OrganizationFinancialReportStatus.NOT_COMPLETED: return { translation: translations.not_completed, badge: BadgeStatus.WARNING, - tooltipContent: 'financial reports exist, but no data has been added', + tooltipContent: translations.tooltip.not_completed, }; case OrganizationFinancialReportStatus.PENDING: return { translation: translations.pending, badge: BadgeStatus.WARNING, - tooltipContent: - 'financial reports exist, admin filled in some information, but ANAF information is not yet ready', + tooltipContent: translations.tooltip.pending, }; default: return { translation: 'Error', badge: BadgeStatus.ERROR, - tooltipContent: - 'Error error error anaf error Error error error anaf error Error error error anaf error ', + tooltipContent: translations.tooltip.error, }; } }; From abde74066817940e2713dad972a1a90b2681a21b Mon Sep 17 00:00:00 2001 From: Dragos-Paul Strat Date: Thu, 22 Aug 2024 12:04:46 +0300 Subject: [PATCH 25/57] feat: [495-1] fix comments --- .../application/dto/create-application.dto.ts | 4 +++ .../application/dto/update-application.dto.ts | 5 ---- .../services/application.service.ts | 8 ++++++ .../services/ong-application.service.ts | 5 ++++ .../apps-store/components/ApplicationForm.tsx | 25 ++++++++++++++----- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/backend/src/modules/application/dto/create-application.dto.ts b/backend/src/modules/application/dto/create-application.dto.ts index 78a303fe9..2262a92b3 100644 --- a/backend/src/modules/application/dto/create-application.dto.ts +++ b/backend/src/modules/application/dto/create-application.dto.ts @@ -11,6 +11,7 @@ import { REGEX } from 'src/common/constants/patterns.constant'; import { ApplicationPullingType } from '../enums/application-pulling-type.enum'; import { ApplicationTypeEnum } from '../enums/ApplicationType.enum'; import { ApplicationLabel } from 'src/shared/entities/application-labels.entity'; +import { ApplicationStatus } from '../enums/application-status.enum'; export class CreateApplicationDto { @IsString() @@ -60,4 +61,7 @@ export class CreateApplicationDto { @IsOptional() applicationLabel: Partial; + + @IsEnum(ApplicationStatus) + status: ApplicationStatus; } diff --git a/backend/src/modules/application/dto/update-application.dto.ts b/backend/src/modules/application/dto/update-application.dto.ts index ce20eaaac..71973a8bc 100644 --- a/backend/src/modules/application/dto/update-application.dto.ts +++ b/backend/src/modules/application/dto/update-application.dto.ts @@ -1,15 +1,10 @@ import { PartialType, OmitType } from '@nestjs/swagger'; import { IsEnum, IsOptional } from 'class-validator'; -import { ApplicationStatus } from '../enums/application-status.enum'; import { CreateApplicationDto } from './create-application.dto'; export class UpdateApplicationDto extends PartialType( OmitType(CreateApplicationDto, ['type']), ) { - @IsEnum(ApplicationStatus) - @IsOptional() - status?: ApplicationStatus; - @IsOptional() cognitoClientId?: string; } diff --git a/backend/src/modules/application/services/application.service.ts b/backend/src/modules/application/services/application.service.ts index 90d5ef357..297875623 100644 --- a/backend/src/modules/application/services/application.service.ts +++ b/backend/src/modules/application/services/application.service.ts @@ -73,9 +73,17 @@ export class ApplicationService { }; } + let applicationLabel = null; + if (createApplicationDto.applicationLabel) { + applicationLabel = await this.saveAndGetApplicationLabel( + createApplicationDto.applicationLabel, + ); + } + // 3. save the app return this.applicationRepository.save({ ...createApplicationDto, + applicationLabel, }); } catch (error) { this.logger.error({ diff --git a/backend/src/modules/application/services/ong-application.service.ts b/backend/src/modules/application/services/ong-application.service.ts index 31c101c39..aea1db8a1 100644 --- a/backend/src/modules/application/services/ong-application.service.ts +++ b/backend/src/modules/application/services/ong-application.service.ts @@ -132,6 +132,11 @@ export class OngApplicationService { 'ongApp.applicationId = application.id AND ongApp.organizationId = :organizationId', { organizationId }, ) + .leftJoin( + '_application-label', + 'applicationLabel', + 'applicationLabel.id = application.application_label_id', + ) .where( 'ongApp.organizationId = :organizationId and ongApp.status != :status', { diff --git a/frontend/src/pages/apps-store/components/ApplicationForm.tsx b/frontend/src/pages/apps-store/components/ApplicationForm.tsx index 9091a03cd..9fa295871 100644 --- a/frontend/src/pages/apps-store/components/ApplicationForm.tsx +++ b/frontend/src/pages/apps-store/components/ApplicationForm.tsx @@ -72,16 +72,29 @@ const ApplicationForm = ({ const ribbonValidation = (inputValue: string) => { if (inputValue.length > 30 && !(errors as Record)[AddAppConfig.applicationLabel.key]) { control.setError(AddAppConfig.applicationLabel.key, { type: 'maxLength', message: t('config.application_label.maxLength') }); + } + + if (inputValue.length > 30) { return false; - } else if (inputValue.length > 2 && inputValue.length < 30 && clearErrors && (errors as Record)[AddAppConfig.applicationLabel.key]) { - clearErrors(AddAppConfig.applicationLabel.key); - return true - } else if (inputValue.length > 0 && inputValue.length < 3 && !(errors as Record)[AddAppConfig.applicationLabel.key]) { + } + + + if (inputValue.length > 2 && inputValue.length < 30 && (errors as Record)[AddAppConfig.applicationLabel.key]) { + clearErrors && clearErrors(AddAppConfig.applicationLabel.key); + } + + if (inputValue.length > 2 && inputValue.length < 30) { + return true; + } + + if (inputValue.length && inputValue.length < 3 && !(errors as Record)[AddAppConfig.applicationLabel.key]) { control.setError(AddAppConfig.applicationLabel.key, { type: 'minLength', message: t('config.application_label.minLength') }); - return false } - console.log('inputValue', inputValue); + if (inputValue.length < 3) { + return false; + } + return true; } From f96b891718031178c7839820da0144936f58441a Mon Sep 17 00:00:00 2001 From: Dragos-Paul Strat Date: Thu, 22 Aug 2024 13:39:00 +0300 Subject: [PATCH 26/57] feat: [495-1] fix comments for PR --- .../src/pages/apps-store/components/ApplicationForm.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/apps-store/components/ApplicationForm.tsx b/frontend/src/pages/apps-store/components/ApplicationForm.tsx index 9fa295871..188984eac 100644 --- a/frontend/src/pages/apps-store/components/ApplicationForm.tsx +++ b/frontend/src/pages/apps-store/components/ApplicationForm.tsx @@ -70,7 +70,9 @@ const ApplicationForm = ({ }; const ribbonValidation = (inputValue: string) => { - if (inputValue.length > 30 && !(errors as Record)[AddAppConfig.applicationLabel.key]) { + const labelError = (errors as Record)[AddAppConfig.applicationLabel.key]; + + if (inputValue.length > 30 && !labelError) { control.setError(AddAppConfig.applicationLabel.key, { type: 'maxLength', message: t('config.application_label.maxLength') }); } @@ -79,7 +81,7 @@ const ApplicationForm = ({ } - if (inputValue.length > 2 && inputValue.length < 30 && (errors as Record)[AddAppConfig.applicationLabel.key]) { + if (inputValue.length > 2 && inputValue.length < 30 && labelError) { clearErrors && clearErrors(AddAppConfig.applicationLabel.key); } @@ -87,7 +89,7 @@ const ApplicationForm = ({ return true; } - if (inputValue.length && inputValue.length < 3 && !(errors as Record)[AddAppConfig.applicationLabel.key]) { + if (inputValue.length && inputValue.length < 3 && !labelError) { control.setError(AddAppConfig.applicationLabel.key, { type: 'minLength', message: t('config.application_label.minLength') }); } From bececb31f996619bbfdc31cb677bdb02e4ae79f3 Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Thu, 22 Aug 2024 13:57:44 +0300 Subject: [PATCH 27/57] feat: [438] Fix wrong calculation on reports CompletionStatus on OrganizationView --- ...724324164379-FixReportsCompletionStatus.ts | 111 ++++++++++++++++++ .../entities/organization-view.entity.ts | 16 +-- 2 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 backend/src/migrations/1724324164379-FixReportsCompletionStatus.ts diff --git a/backend/src/migrations/1724324164379-FixReportsCompletionStatus.ts b/backend/src/migrations/1724324164379-FixReportsCompletionStatus.ts new file mode 100644 index 000000000..d8c3aaedf --- /dev/null +++ b/backend/src/migrations/1724324164379-FixReportsCompletionStatus.ts @@ -0,0 +1,111 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class FixReportsCompletionStatus1724324164379 + implements MigrationInterface +{ + name = 'FixReportsCompletionStatus1724324164379'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'OrganizationView', 'public'], + ); + await queryRunner.query(`DROP VIEW "OrganizationView"`); + await queryRunner.query(`CREATE VIEW "OrganizationView" AS + SELECT + "organization".id AS "id", + "organization".status AS "status", + "organization".created_on AS "createdOn", + CASE + WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != 'Completed' OR "organization_financial".status IS NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "report".status != 'Completed' OR "report".status IS NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "partner".status != 'Completed' OR "partner".status IS NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "investor".status != 'Completed' OR "investor".status IS NULL THEN 1 END) > 0 + THEN 'Not completed' + ELSE 'Completed' + END AS "completionStatus", + "organization_general".name AS "name", + "organization_general".alias AS "alias", + COUNT("user".id) AS "userCount", + "organization_general".logo AS "logo", + MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01'))) AS "updatedOn" + FROM + "organization" "organization" + LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id + LEFT JOIN "user" "user" ON "user".organization_id = "organization".id + AND "user".deleted_on IS NULL + AND "user".ROLE = 'employee' + AND "user".status != 'pending' + LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId" + LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id + LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId" + LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId" + LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId" + WHERE + "organization".status != 'pending' + GROUP BY + "organization".id, + "organization_general".id + `); + await queryRunner.query( + `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + 'public', + 'VIEW', + 'OrganizationView', + 'SELECT\n "organization".id AS "id",\n "organization".status AS "status",\n "organization".created_on AS "createdOn",\n \tCASE \n WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != \'Completed\' OR "organization_financial".status IS NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "report".status != \'Completed\' OR "report".status IS NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "partner".status != \'Completed\' OR "partner".status IS NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "investor".status != \'Completed\' OR "investor".status IS NULL THEN 1 END) > 0\n THEN \'Not completed\'\n ELSE \'Completed\'\n \tEND AS "completionStatus",\n "organization_general".name AS "name",\n "organization_general".alias AS "alias",\n COUNT("user".id) AS "userCount",\n "organization_general".logo AS "logo",\n MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\'))) AS "updatedOn"\n FROM\n "organization" "organization"\n LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id\n LEFT JOIN "user" "user" ON "user".organization_id = "organization".id\n AND "user".deleted_on IS NULL\n AND "user".ROLE = \'employee\'\n AND "user".status != \'pending\'\n LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId"\n LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id\n LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId"\n LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId"\n LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId"\n WHERE\n "organization".status != \'pending\'\n GROUP BY\n "organization".id,\n "organization_general".id', + ], + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'OrganizationView', 'public'], + ); + await queryRunner.query(`DROP VIEW "OrganizationView"`); + await queryRunner.query(`CREATE VIEW "OrganizationView" AS SELECT + "organization".id AS "id", + "organization".status AS "status", + "organization".created_on AS "createdOn", + CASE WHEN MIN(COALESCE("organization_financial".status, 'Not Completed')) != 'Completed' + OR MIN(COALESCE("report".status, 'Not Completed')) != 'Completed' + OR MIN(COALESCE("partner".status, 'Not Completed')) != 'Completed' + OR MIN(COALESCE("investor".status, 'Not Completed')) != 'Completed' THEN + 'Not completed' + ELSE + 'Completed' + END AS "completionStatus", + "organization_general".name AS "name", + "organization_general".alias AS "alias", + COUNT("user".id) AS "userCount", + "organization_general".logo AS "logo", + MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01'))) AS "updatedOn" + FROM + "organization" "organization" + LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id + LEFT JOIN "user" "user" ON "user".organization_id = "organization".id + AND "user".deleted_on IS NULL + AND "user".ROLE = 'employee' + AND "user".status != 'pending' + LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId" + LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id + LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId" + LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId" + LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId" + WHERE + "organization".status != 'pending' + GROUP BY + "organization".id, + "organization_general".id`); + await queryRunner.query( + `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + 'public', + 'VIEW', + 'OrganizationView', + 'SELECT\n "organization".id AS "id",\n "organization".status AS "status",\n "organization".created_on AS "createdOn",\n CASE WHEN MIN(COALESCE("organization_financial".status, \'Not Completed\')) != \'Completed\'\n OR MIN(COALESCE("report".status, \'Not Completed\')) != \'Completed\'\n OR MIN(COALESCE("partner".status, \'Not Completed\')) != \'Completed\'\n OR MIN(COALESCE("investor".status, \'Not Completed\')) != \'Completed\' THEN\n \'Not completed\'\n ELSE\n \'Completed\'\n END AS "completionStatus",\n "organization_general".name AS "name",\n "organization_general".alias AS "alias",\n COUNT("user".id) AS "userCount",\n "organization_general".logo AS "logo",\n MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\'))) AS "updatedOn"\n FROM\n "organization" "organization"\n LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id\n LEFT JOIN "user" "user" ON "user".organization_id = "organization".id\n AND "user".deleted_on IS NULL\n AND "user".ROLE = \'employee\'\n AND "user".status != \'pending\'\n LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId"\n LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id\n LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId"\n LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId"\n LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId"\n WHERE\n "organization".status != \'pending\'\n GROUP BY\n "organization".id,\n "organization_general".id', + ], + ); + } +} diff --git a/backend/src/modules/organization/entities/organization-view.entity.ts b/backend/src/modules/organization/entities/organization-view.entity.ts index 0249dd310..e50378257 100644 --- a/backend/src/modules/organization/entities/organization-view.entity.ts +++ b/backend/src/modules/organization/entities/organization-view.entity.ts @@ -8,14 +8,14 @@ import { OrganizationStatus } from '../enums/organization-status.enum'; "organization".id AS "id", "organization".status AS "status", "organization".created_on AS "createdOn", - CASE WHEN MIN(COALESCE("organization_financial".status, 'Not Completed')) != 'Completed' - OR MIN(COALESCE("report".status, 'Not Completed')) != 'Completed' - OR MIN(COALESCE("partner".status, 'Not Completed')) != 'Completed' - OR MIN(COALESCE("investor".status, 'Not Completed')) != 'Completed' THEN - 'Not completed' - ELSE - 'Completed' - END AS "completionStatus", + CASE + WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != 'Completed' OR "organization_financial".status IS NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "report".status != 'Completed' OR "report".status IS NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "partner".status != 'Completed' OR "partner".status IS NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "investor".status != 'Completed' OR "investor".status IS NULL THEN 1 END) > 0 + THEN 'Not completed' + ELSE 'Completed' + END AS "completionStatus", "organization_general".name AS "name", "organization_general".alias AS "alias", COUNT("user".id) AS "userCount", From d9dd7a8024f1f13e78f475d48b3409cf1c120af0 Mon Sep 17 00:00:00 2001 From: Dragos-Paul Strat Date: Thu, 22 Aug 2024 14:28:48 +0300 Subject: [PATCH 28/57] fix: [611] add checks for addresses of create/edit organization general --- .../components/CreateOrganizationGeneral.tsx | 6 ++++++ .../components/OrganizationGeneral/OrganizationGeneral.tsx | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/frontend/src/pages/create-organziation/components/CreateOrganizationGeneral.tsx b/frontend/src/pages/create-organziation/components/CreateOrganizationGeneral.tsx index 766e5ad90..5a4f22b18 100644 --- a/frontend/src/pages/create-organziation/components/CreateOrganizationGeneral.tsx +++ b/frontend/src/pages/create-organziation/components/CreateOrganizationGeneral.tsx @@ -92,6 +92,8 @@ const CreateOrganizationGeneral = () => { setValue('organizationAddress', address); setValue('organizationCity', city); setValue('organizationCounty', county); + + setOrganizationCounty(county); } }, [watchHasSameAddress]); @@ -432,6 +434,7 @@ const CreateOrganizationGeneral = () => { selected={value} onChange={onChange} readonly={readonly} + disabled={!county} /> ); }} @@ -644,6 +647,7 @@ const CreateOrganizationGeneral = () => { onChange: onChange, id: 'create-organization-general__org-organization-address', }} + disabled={watchHasSameAddress} readonly={readonly} /> ); @@ -671,6 +675,7 @@ const CreateOrganizationGeneral = () => { handleSetOrganizationCounty(e) }} readonly={readonly} + disabled={watchHasSameAddress} /> ); }} @@ -693,6 +698,7 @@ const CreateOrganizationGeneral = () => { selected={value} onChange={onChange} readonly={readonly} + disabled={watchHasSameAddress || !organizationCounty} /> ); }} diff --git a/frontend/src/pages/organization/components/OrganizationGeneral/OrganizationGeneral.tsx b/frontend/src/pages/organization/components/OrganizationGeneral/OrganizationGeneral.tsx index 26bd12455..62f6909b3 100644 --- a/frontend/src/pages/organization/components/OrganizationGeneral/OrganizationGeneral.tsx +++ b/frontend/src/pages/organization/components/OrganizationGeneral/OrganizationGeneral.tsx @@ -478,6 +478,7 @@ const OrganizationGeneral = () => { selected={value} onChange={onChange} readonly={readonly} + disabled={!county} /> ); }} @@ -703,6 +704,7 @@ const OrganizationGeneral = () => { placeholder={OrganizationGeneralConfig.organizationAddress.config.placeholder} onChange={onChange} id="create-organization-general__org-organization-address" + disabled={watchHasSameAddress} /> ); }} @@ -729,6 +731,7 @@ const OrganizationGeneral = () => { handleSetOrganizationCounty(e) }} readonly={readonly} + disabled={watchHasSameAddress} /> ); }} @@ -751,6 +754,7 @@ const OrganizationGeneral = () => { selected={value} onChange={onChange} readonly={readonly} + disabled={watchHasSameAddress || !organizationCounty} /> ); }} From d7c0126e981e055b3483daf99c700ab3b56ca711 Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Thu, 22 Aug 2024 15:39:36 +0300 Subject: [PATCH 29/57] feat: [438] Send email on 1st of june to all organizations with missing reports information --- .../src/mail/constants/template.constants.ts | 34 ++++++++--- backend/src/mail/templates/mail-template.hbs | 5 +- ...041291-AddAdminEmailToOrganizationView.ts} | 9 +-- .../entities/organization-view.entity.ts | 6 +- .../organization/services/crons.service.ts | 59 ++++++++++++------- 5 files changed, 80 insertions(+), 33 deletions(-) rename backend/src/migrations/{1724324164379-FixReportsCompletionStatus.ts => 1724330041291-AddAdminEmailToOrganizationView.ts} (82%) diff --git a/backend/src/mail/constants/template.constants.ts b/backend/src/mail/constants/template.constants.ts index 3f2cbcd93..89a10acae 100644 --- a/backend/src/mail/constants/template.constants.ts +++ b/backend/src/mail/constants/template.constants.ts @@ -101,16 +101,36 @@ export const MAIL_OPTIONS: Record = { }, }, }, - REMIND_TO_COMPLETE_FINANCIAL_DATA: { + REMIND_TO_UPDATE_ORGANIZATION_REPORTS: { template: ORGANIZATION_REQUEST, - subject: 'Va reamintim sa actualizati datele financiare in ONG Hub!', + subject: + 'Actualizați profilul organizației până pe 30 iunie pentru a evita suspendarea contului', context: { - title: 'Va reamintim sa actualizati datele financiare in ONG Hub!', - subtitle: () => - 'Pentru a va pastra accesul in contul de ONG Hub, va rugam sa actualizati datele financiare pana la data de 30 Iunie.', + title: + 'Actualizați profilul organizației până pe 30 iunie pentru a evita suspendarea contului', + subtitle: () => ` +

Bună,

+ +

Ne bucurăm că ești parte din comunitatea NGO Hub!

+ +

Vrem să îți reamintim că este important să îți actualizezi datele din profilul tău din NGO Hub până la data de 30 iunie ${new Date().getFullYear()}.

+ +

Dacă nu reușești să faci această actualizare până la termenul limită, contul tău va fi suspendat temporar.

+ +

Aceștia sunt pașii pe care trebuie să îi urmezi pentru actualizare:

+ +
    +
  1. Conectează-te la contul tău NGO Hub
  2. +
  3. Mergi la secțiunea „Organizația mea"
  4. +
  5. Verifică și actualizează toate informațiile din secțiunea „Informații financiare" și „ONG-ul în numere".
  6. +
  7. Salvează modificările
  8. +
+ +

Dacă ai nevoie de ajutor sau ai orice fel de întrebare, ne poți contacta oricând la civic@code4.ro sau poți programa o sesiune Civic Tech 911 direct din contul NGO Hub al organizației.

+ `, cta: { - link: () => `${process.env.ONGHUB_URL}/organization/financial`, - label: 'Completeaza informatiile financiare', + link: () => `${process.env.ONGHUB_URL}/organization`, + label: 'Intra in cont', }, }, }, diff --git a/backend/src/mail/templates/mail-template.hbs b/backend/src/mail/templates/mail-template.hbs index 81e6c1f24..ebd34c568 100644 --- a/backend/src/mail/templates/mail-template.hbs +++ b/backend/src/mail/templates/mail-template.hbs @@ -66,12 +66,15 @@ {{> header}}

{{title}}

-

{{subtitle}}

+

{{{subtitle}}}

{{#if cta}} {{/if}} + +

Cu drag,

+

Echipa NGO Hub

{{> footer}}
diff --git a/backend/src/migrations/1724324164379-FixReportsCompletionStatus.ts b/backend/src/migrations/1724330041291-AddAdminEmailToOrganizationView.ts similarity index 82% rename from backend/src/migrations/1724324164379-FixReportsCompletionStatus.ts rename to backend/src/migrations/1724330041291-AddAdminEmailToOrganizationView.ts index d8c3aaedf..f385c9637 100644 --- a/backend/src/migrations/1724324164379-FixReportsCompletionStatus.ts +++ b/backend/src/migrations/1724330041291-AddAdminEmailToOrganizationView.ts @@ -1,9 +1,9 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class FixReportsCompletionStatus1724324164379 +export class AddAdminEmailToOrganizationView1724330041291 implements MigrationInterface { - name = 'FixReportsCompletionStatus1724324164379'; + name = 'AddAdminEmailToOrganizationView1724330041291'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( @@ -21,11 +21,12 @@ export class FixReportsCompletionStatus1724324164379 OR COUNT(DISTINCT CASE WHEN "report".status != 'Completed' OR "report".status IS NULL THEN 1 END) > 0 OR COUNT(DISTINCT CASE WHEN "partner".status != 'Completed' OR "partner".status IS NULL THEN 1 END) > 0 OR COUNT(DISTINCT CASE WHEN "investor".status != 'Completed' OR "investor".status IS NULL THEN 1 END) > 0 - THEN 'Not completed' + THEN 'Not Completed' ELSE 'Completed' END AS "completionStatus", "organization_general".name AS "name", "organization_general".alias AS "alias", + "organization_general".email AS "adminEmail", COUNT("user".id) AS "userCount", "organization_general".logo AS "logo", MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01'))) AS "updatedOn" @@ -53,7 +54,7 @@ export class FixReportsCompletionStatus1724324164379 'public', 'VIEW', 'OrganizationView', - 'SELECT\n "organization".id AS "id",\n "organization".status AS "status",\n "organization".created_on AS "createdOn",\n \tCASE \n WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != \'Completed\' OR "organization_financial".status IS NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "report".status != \'Completed\' OR "report".status IS NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "partner".status != \'Completed\' OR "partner".status IS NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "investor".status != \'Completed\' OR "investor".status IS NULL THEN 1 END) > 0\n THEN \'Not completed\'\n ELSE \'Completed\'\n \tEND AS "completionStatus",\n "organization_general".name AS "name",\n "organization_general".alias AS "alias",\n COUNT("user".id) AS "userCount",\n "organization_general".logo AS "logo",\n MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\'))) AS "updatedOn"\n FROM\n "organization" "organization"\n LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id\n LEFT JOIN "user" "user" ON "user".organization_id = "organization".id\n AND "user".deleted_on IS NULL\n AND "user".ROLE = \'employee\'\n AND "user".status != \'pending\'\n LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId"\n LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id\n LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId"\n LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId"\n LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId"\n WHERE\n "organization".status != \'pending\'\n GROUP BY\n "organization".id,\n "organization_general".id', + 'SELECT\n "organization".id AS "id",\n "organization".status AS "status",\n "organization".created_on AS "createdOn",\n \tCASE \n WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != \'Completed\' OR "organization_financial".status IS NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "report".status != \'Completed\' OR "report".status IS NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "partner".status != \'Completed\' OR "partner".status IS NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "investor".status != \'Completed\' OR "investor".status IS NULL THEN 1 END) > 0\n THEN \'Not Completed\'\n ELSE \'Completed\'\n \tEND AS "completionStatus",\n "organization_general".name AS "name",\n "organization_general".alias AS "alias",\n "organization_general".email AS "adminEmail",\n COUNT("user".id) AS "userCount",\n "organization_general".logo AS "logo",\n MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\'))) AS "updatedOn"\n FROM\n "organization" "organization"\n LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id\n LEFT JOIN "user" "user" ON "user".organization_id = "organization".id\n AND "user".deleted_on IS NULL\n AND "user".ROLE = \'employee\'\n AND "user".status != \'pending\'\n LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId"\n LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id\n LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId"\n LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId"\n LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId"\n WHERE\n "organization".status != \'pending\'\n GROUP BY\n "organization".id,\n "organization_general".id', ], ); } diff --git a/backend/src/modules/organization/entities/organization-view.entity.ts b/backend/src/modules/organization/entities/organization-view.entity.ts index e50378257..8f233da9f 100644 --- a/backend/src/modules/organization/entities/organization-view.entity.ts +++ b/backend/src/modules/organization/entities/organization-view.entity.ts @@ -13,11 +13,12 @@ import { OrganizationStatus } from '../enums/organization-status.enum'; OR COUNT(DISTINCT CASE WHEN "report".status != 'Completed' OR "report".status IS NULL THEN 1 END) > 0 OR COUNT(DISTINCT CASE WHEN "partner".status != 'Completed' OR "partner".status IS NULL THEN 1 END) > 0 OR COUNT(DISTINCT CASE WHEN "investor".status != 'Completed' OR "investor".status IS NULL THEN 1 END) > 0 - THEN 'Not completed' + THEN 'Not Completed' ELSE 'Completed' END AS "completionStatus", "organization_general".name AS "name", "organization_general".alias AS "alias", + "organization_general".email AS "adminEmail", COUNT("user".id) AS "userCount", "organization_general".logo AS "logo", MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01'))) AS "updatedOn" @@ -67,4 +68,7 @@ export class OrganizationView { @ViewColumn() completionStatus: CompletionStatus; + + @ViewColumn() + adminEmail: string; } diff --git a/backend/src/modules/organization/services/crons.service.ts b/backend/src/modules/organization/services/crons.service.ts index e0dd9bc35..a31e04959 100644 --- a/backend/src/modules/organization/services/crons.service.ts +++ b/backend/src/modules/organization/services/crons.service.ts @@ -1,12 +1,21 @@ import { Injectable, Logger } from '@nestjs/common'; import { OrganizationRepository } from '../repositories/organization.repository'; -import { OrganizationFinancialRepository } from '../repositories'; +import { + OrganizationFinancialRepository, + OrganizationViewRepository, +} from '../repositories'; import { OrganizationFinancialService } from './organization-financial.service'; import { OrganizationReportService } from './organization-report.service'; import * as Sentry from '@sentry/node'; import { Cron, CronExpression } from '@nestjs/schedule'; import { MailService } from 'src/mail/services/mail.service'; import { MAIL_OPTIONS } from 'src/mail/constants/template.constants'; +import { EntityManager, In, Not, getManager } from 'typeorm'; +import { + CompletionStatus, + OrganizationFinancialReportStatus, +} from '../enums/organization-financial-completion.enum'; +import { InjectEntityManager } from '@nestjs/typeorm'; @Injectable() export class OrganizationCronsService { @@ -14,6 +23,7 @@ export class OrganizationCronsService { constructor( private readonly organizationRepository: OrganizationRepository, + private readonly organizationViewRepository: OrganizationViewRepository, private readonly organizationFinancialService: OrganizationFinancialService, private readonly organizationReportService: OrganizationReportService, private readonly mailService: MailService, @@ -72,26 +82,35 @@ export class OrganizationCronsService { } } + /** + * + * Organizations must complete their reports until 30th of June each year, otherwise the account will be suspended + * + * The reports are: + * 1. Financial Report + * 2. ONG In Numere + * 2.1. Reports + * 2.2. Investors + * 2.3. Partners + * + * + * On 1st Of june, we send an email to all organization which didn't fully complete their reports. + * + */ @Cron('0 12 1 6 *') // 1st of June, 12 PM server time - async sendEmailToRemindFinancialDataCompletion() { - // 1. Get all organizations with are missing the previous year the financial data and reports - const organizations = await this.organizationRepository.getMany({ - relations: { - organizationGeneral: true, - organizationFinancial: true, - }, - }); + async sendEmailToRemindOrganizationProfileUpdate() { + // 1. Get all organizations missin the completion of financial data and reports + const organizations: { adminEmail: string }[] = + await this.organizationViewRepository.getMany({ + where: { + completionStatus: CompletionStatus.NOT_COMPLETED, + }, + select: { + adminEmail: true, + }, + }); - // Filter organization to send email only to those who have the reports available for the last year - // Some organizations created in the current year will not have the data available - const receivers = organizations - .filter((org) => { - return org.organizationFinancial.some( - (financialReport) => - financialReport.year === new Date().getFullYear() - 1, - ); - }) - .map((org) => org.organizationGeneral.email); + const receivers = organizations.map((org) => org.adminEmail); const { subject, @@ -101,7 +120,7 @@ export class OrganizationCronsService { subtitle, cta: { link, label }, }, - } = MAIL_OPTIONS.REMIND_TO_COMPLETE_FINANCIAL_DATA; + } = MAIL_OPTIONS.REMIND_TO_UPDATE_ORGANIZATION_REPORTS; for (let email of receivers) { await this.mailService.sendEmail({ From 8638bc4a0ab8b77cfb73df62d8db862f8efb250a Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Thu, 22 Aug 2024 16:17:55 +0300 Subject: [PATCH 30/57] feat: [438] Send notification to NGO admins after we get data from ANAF and the status becomes Invalid --- .../src/mail/constants/template.constants.ts | 35 ++++++++++- .../constants/events.contants.ts | 1 + .../invalid-financial-reports-event.class.ts | 7 +++ .../services/notifications.service.ts | 50 ++++++++++++---- .../organization-financial.service.ts | 60 +++++++++++++------ 5 files changed, 122 insertions(+), 31 deletions(-) create mode 100644 backend/src/modules/notifications/events/invalid-financial-reports-event.class.ts diff --git a/backend/src/mail/constants/template.constants.ts b/backend/src/mail/constants/template.constants.ts index 89a10acae..19f942519 100644 --- a/backend/src/mail/constants/template.constants.ts +++ b/backend/src/mail/constants/template.constants.ts @@ -126,7 +126,40 @@ export const MAIL_OPTIONS: Record = {
  • Salvează modificările
  • -

    Dacă ai nevoie de ajutor sau ai orice fel de întrebare, ne poți contacta oricând la civic@code4.ro sau poți programa o sesiune Civic Tech 911 direct din contul NGO Hub al organizației.

    +

    Dacă ai nevoie de ajutor sau ai orice fel de întrebare, ne poți contacta oricând la civic@code4.ro sau poți programa o sesiune Civic Tech 911 direct din contul NGO Hub al organizației.

    + `, + cta: { + link: () => `${process.env.ONGHUB_URL}/organization`, + label: 'Intra in cont', + }, + }, + }, + NOTIFY_FOR_UNAVAILABLE_OR_INVALID_FINANCIAL_INFORMATION: { + template: ORGANIZATION_REQUEST, + subject: + 'Acțiune necesară: Te rugăm să corectezi datele din profilul organizației din NGO Hub', + context: { + title: + 'Acțiune necesară: Te rugăm să corectezi datele din profilul organizației din NGO Hub', + subtitle: () => ` +

    Bună,

    + +

    Ne bucurăm că ești parte din comunitatea NGO Hub!

    + +

    Am observat că unele dintre informațiile recent actualizate din profilul tău de pe NGO Hub par a fi invalide sau incomplete. Pentru a ne asigura că ai acces neîntrerupt la contul tău, te rugăm să îți actualizezi și corectezi datele cât mai curând posibil.

    + +

    Aceștia sunt pașii pe care trebuie să îi urmezi pentru actualizare:

    + +
      +
    1. Conectează-te la contul tău NGO Hub
    2. +
    3. Mergi la secțiunea „Organizația mea"
    4. +
    5. Verifică și actualizează toate informațiile din secțiunea „Informații financiare" și „ONG-ul în numere".
    6. +
    7. Salvează modificările
    8. +
    + +

    Este important să faci aceste actualizări înainte de 30 iunie 2024 pentru a evita suspendarea contului tău NGO Hub.

    + +

    Dacă ai nevoie de ajutor sau ai orice fel de întrebare, ne poți contacta oricând la civic@code4.ro sau poți programa o sesiune Civic Tech 911 direct din contul NGO Hub al organizației.

    `, cta: { link: () => `${process.env.ONGHUB_URL}/organization`, diff --git a/backend/src/modules/notifications/constants/events.contants.ts b/backend/src/modules/notifications/constants/events.contants.ts index 6531df1c0..9991257ef 100644 --- a/backend/src/modules/notifications/constants/events.contants.ts +++ b/backend/src/modules/notifications/constants/events.contants.ts @@ -6,4 +6,5 @@ export const EVENTS = { DISABLE_ORGANIZATION_REQUEST: 'disable.ong.request', RESTRICT_ORGANIZATION: 'restrict.organization', REQUEST_APPLICATION_ACCESS: 'application.request', + INVALID_FINANCIAL_REPORTS: 'organization.financial.reports.invalid', }; diff --git a/backend/src/modules/notifications/events/invalid-financial-reports-event.class.ts b/backend/src/modules/notifications/events/invalid-financial-reports-event.class.ts new file mode 100644 index 000000000..4157c3471 --- /dev/null +++ b/backend/src/modules/notifications/events/invalid-financial-reports-event.class.ts @@ -0,0 +1,7 @@ +export default class InvalidFinancialReportsEvent { + constructor(private _email: string) {} + + public get email() { + return this._email; + } +} diff --git a/backend/src/modules/notifications/services/notifications.service.ts b/backend/src/modules/notifications/services/notifications.service.ts index b046a005b..a83a48f9a 100644 --- a/backend/src/modules/notifications/services/notifications.service.ts +++ b/backend/src/modules/notifications/services/notifications.service.ts @@ -18,6 +18,7 @@ import RejectOngRequestEvent from '../events/reject-ong-request-event.class'; import RestrictOngEvent from '../events/restrict-ong-event.class'; import ApplicationRequestEvent from '../events/ong-requested-application-access-event.class'; import * as Sentry from '@sentry/node'; +import InvalidFinancialReportsEvent from '../events/invalid-financial-reports-event.class'; @Injectable() export class NotificationsService { @@ -38,9 +39,8 @@ export class NotificationsService { where: { role: Role.SUPER_ADMIN }, }); - const organziation = await this.organizationService.findWithRelations( - organizationId, - ); + const organziation = + await this.organizationService.findWithRelations(organizationId); // send email to admin to delete the application const { @@ -230,9 +230,8 @@ export class NotificationsService { try { const { organizationId } = payload; - const organization = await this.organizationService.findWithUsers( - organizationId, - ); + const organization = + await this.organizationService.findWithUsers(organizationId); const admins = organization.users.filter( (user) => user.role === Role.ADMIN, @@ -263,15 +262,17 @@ export class NotificationsService { } @OnEvent(EVENTS.REQUEST_APPLICATION_ACCESS) - async handleRequestApplicationAccess({applicationName, organizationId}: ApplicationRequestEvent) { - + async handleRequestApplicationAccess({ + applicationName, + organizationId, + }: ApplicationRequestEvent) { try { const superAdmins = await this.userService.findMany({ where: { role: Role.SUPER_ADMIN }, }); const organization = await this.organizationService.find(organizationId, { - relations: ['organizationGeneral'] + relations: ['organizationGeneral'], }); const { @@ -298,7 +299,6 @@ export class NotificationsService { }, }, }); - } catch (error) { Sentry.captureException(error); this.logger.error({ @@ -306,8 +306,34 @@ export class NotificationsService { error, }); } - } + @OnEvent(EVENTS.INVALID_FINANCIAL_REPORTS) + async handleRemindToCompleteFinancialData( + payload: InvalidFinancialReportsEvent, + ) { + const { + subject, + template, + context: { + title, + subtitle, + cta: { link, label }, + }, + } = MAIL_OPTIONS.NOTIFY_FOR_UNAVAILABLE_OR_INVALID_FINANCIAL_INFORMATION; + + await this.mailService.sendEmail({ + to: payload.email, + template, + subject, + context: { + title, + subtitle: subtitle(), + cta: { + link: link(), + label, + }, + }, + }); + } } - diff --git a/backend/src/modules/organization/services/organization-financial.service.ts b/backend/src/modules/organization/services/organization-financial.service.ts index 35dbb32cd..57696a9b9 100644 --- a/backend/src/modules/organization/services/organization-financial.service.ts +++ b/backend/src/modules/organization/services/organization-financial.service.ts @@ -15,11 +15,13 @@ import { AnafService, FinancialInformation, } from 'src/shared/services/anaf.service'; -import { OnEvent } from '@nestjs/event-emitter'; +import { EventEmitter2, OnEvent } from '@nestjs/event-emitter'; import CUIChangedEvent from '../events/CUI-changed-event.class'; import { ORGANIZATION_EVENTS } from '../constants/events.constants'; import * as Sentry from '@sentry/node'; import { In, Not } from 'typeorm'; +import { EVENTS } from 'src/modules/notifications/constants/events.contants'; +import InvalidFinancialReportsEvent from 'src/modules/notifications/events/invalid-financial-reports-event.class'; @Injectable() export class OrganizationFinancialService { @@ -29,10 +31,8 @@ export class OrganizationFinancialService { private readonly organizationFinancialRepository: OrganizationFinancialRepository, private readonly organizationRepository: OrganizationRepository, private readonly anafService: AnafService, - ) { - // this.handleRegenerateFinancial({ organizationId: 170, cui: '29244879' }); - // this.refetchANAFDataForFinancialReports(); - } + private readonly eventEmitter: EventEmitter2, + ) {} // TODO: Deprecated, we don't allow changing the CUI anymore, so this is obsolete. To be discussed and deleted @OnEvent(ORGANIZATION_EVENTS.CUI_CHANGED) @@ -256,7 +256,7 @@ export class OrganizationFinancialService { type OrganizationsWithMissingANAFData = { id: number; organizationFinancial: OrganizationFinancial[]; - organizationGeneral: { cui: string }; + organizationGeneral: { cui: string; email: string }; }; const data: OrganizationsWithMissingANAFData[] = @@ -274,6 +274,7 @@ export class OrganizationFinancialService { id: true, organizationGeneral: { cui: true, + email: true, }, organizationFinancial: true, }, @@ -311,6 +312,9 @@ export class OrganizationFinancialService { {}, ); + // A notification will be sent to the organization if the completed data is invalid + let sendNotificationForInvalidData = false; + // Iterate over all years and call ANAF for (let year of Object.keys(years)) { // 2. Fetch data from ANAF for the given CUI and YEAR @@ -321,34 +325,54 @@ export class OrganizationFinancialService { if (anafData) { // We have the data, upadate the reports. First "Income" - if (years[year].Income) + if (years[year].Income) { + const reportStatus = this.determineReportStatus( + years[year].Income.existingTotal, + anafData.totalIncome, + true, + ); await this.organizationFinancialRepository.updateOne({ id: years[year].Income.id, total: anafData.totalIncome, numberOfEmployees: anafData.numberOfEmployees, synched_anaf: true, - reportStatus: this.determineReportStatus( - years[year].Income.existingTotal, - anafData.totalIncome, - true, - ), + reportStatus, }); + sendNotificationForInvalidData = sendNotificationForInvalidData + ? true + : reportStatus !== OrganizationFinancialReportStatus.COMPLETED; + } + // Second: "Expense" - if (years[year].Expense) + if (years[year].Expense) { + const reportStatus = this.determineReportStatus( + years[year].Expense.existingTotal, + anafData.totalExpense, + true, + ); await this.organizationFinancialRepository.updateOne({ id: years[year].Expense.id, total: anafData.totalExpense, numberOfEmployees: anafData.numberOfEmployees, synched_anaf: true, - reportStatus: this.determineReportStatus( - years[year].Expense.existingTotal, - anafData.totalExpense, - true, - ), + reportStatus, }); + + sendNotificationForInvalidData = sendNotificationForInvalidData + ? true + : reportStatus !== OrganizationFinancialReportStatus.COMPLETED; + } } } + + // In case one of the report is invalid, we notify the ADMIN to modify them + if (sendNotificationForInvalidData) { + this.eventEmitter.emit( + EVENTS.INVALID_FINANCIAL_REPORTS, + new InvalidFinancialReportsEvent(org.organizationGeneral.email), + ); + } } catch (err) { Sentry.captureException(err, { extra: { From 7840df410015fe2a8c6d1e2fdbc79bb967640a44 Mon Sep 17 00:00:00 2001 From: luciatugui Date: Thu, 22 Aug 2024 17:05:19 +0300 Subject: [PATCH 31/57] style: modify success and warning tooltip colors --- frontend/src/components/status-badge/StatusBadge.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/status-badge/StatusBadge.tsx b/frontend/src/components/status-badge/StatusBadge.tsx index b63c2a437..9f4812787 100644 --- a/frontend/src/components/status-badge/StatusBadge.tsx +++ b/frontend/src/components/status-badge/StatusBadge.tsx @@ -53,7 +53,7 @@ const StatusBadge = ({ status, value, tooltip, tooltipContent = '' }: StatusBadg maxWidth: '250px', backgroundColor: status === BadgeStatus.SUCCESS - ? colors.green[200] + ? colors.green[300] : status === BadgeStatus.ERROR ? colors.red[200] : colors.yellow[200], @@ -62,7 +62,7 @@ const StatusBadge = ({ status, value, tooltip, tooltipContent = '' }: StatusBadg ? colors.green[900] : status === BadgeStatus.ERROR ? colors.red[900] - : colors.yellow[900], + : colors.amber[600], }} /> )} From d8552315f1db850850c66972439a3e0ab6dccd91 Mon Sep 17 00:00:00 2001 From: Dragos-Paul Strat Date: Thu, 22 Aug 2024 17:30:37 +0300 Subject: [PATCH 32/57] feat: [438] add weekly reminder cron for reports --- .../src/mail/constants/template.constants.ts | 31 +++++++++++++ .../organization/services/crons.service.ts | 43 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/backend/src/mail/constants/template.constants.ts b/backend/src/mail/constants/template.constants.ts index 19f942519..748b3dfb2 100644 --- a/backend/src/mail/constants/template.constants.ts +++ b/backend/src/mail/constants/template.constants.ts @@ -134,6 +134,37 @@ export const MAIL_OPTIONS: Record = { }, }, }, + WEEKLY_REMINDER_TO_UPDATE_ORGANIZATION_REPORTS: { + template: ORGANIZATION_REQUEST, + subject: + 'Actualizați profilul organizației până pe 30 iunie pentru a evita suspendarea contului', + context: { + title: + 'Actualizați profilul organizației până pe 30 iunie pentru a evita suspendarea contului', + subtitle: () => ` +

    Bună,

    + +

    Ne bucurăm că ești parte din comunitatea NGO Hub!

    + +

    Îți reamintim că termenul limită pentru actualizarea datelor din profilul tău este 30 iunie ${new Date().getFullYear()}. Pentru a-ți păstra contul activ, te rugăm să îți actualizezi informațiile cât mai curând posibil.

    + +

    Aceștia sunt pașii pe care trebuie să îi urmezi pentru actualizare:

    + +
      +
    1. Conectează-te la contul tău NGO Hub
    2. +
    3. Mergi la secțiunea „Organizația mea"
    4. +
    5. Verifică și actualizează toate informațiile din secțiunea „Informații financiare" și „ONG-ul în numere".
    6. +
    7. Salvează modificările
    8. +
    + +

    Dacă ai nevoie de ajutor sau ai orice fel de întrebare, ne poți contacta oricând la civic@code4.ro sau poți programa o sesiune Civic Tech 911 direct din contul NGO Hub al organizației.

    + `, + cta: { + link: () => `${process.env.ONGHUB_URL}/organization`, + label: 'Intra in cont', + }, + }, + }, NOTIFY_FOR_UNAVAILABLE_OR_INVALID_FINANCIAL_INFORMATION: { template: ORGANIZATION_REQUEST, subject: diff --git a/backend/src/modules/organization/services/crons.service.ts b/backend/src/modules/organization/services/crons.service.ts index a31e04959..caa5b9c6e 100644 --- a/backend/src/modules/organization/services/crons.service.ts +++ b/backend/src/modules/organization/services/crons.service.ts @@ -139,6 +139,49 @@ export class OrganizationCronsService { } } + // Every monday at 8:00 AM (server time) from 8th to 30th of June + @Cron('0 8 8-30 6 1') + async sendReminderForOrganizationProfileUpdate() { + // 1. Get all organizations missin the completion of financial data and reports + const organizations: { adminEmail: string }[] = + await this.organizationViewRepository.getMany({ + where: { + completionStatus: CompletionStatus.NOT_COMPLETED, + }, + select: { + adminEmail: true, + }, + }); + + const receivers = organizations.map((org) => org.adminEmail); + + const { + subject, + template, + context: { + title, + subtitle, + cta: { link, label }, + }, + } = MAIL_OPTIONS.WEEKLY_REMINDER_TO_UPDATE_ORGANIZATION_REPORTS; + + for (let email of receivers) { + await this.mailService.sendEmail({ + to: email, + template, + subject, + context: { + title, + subtitle: subtitle(), + cta: { + link: link(), + label, + }, + }, + }); + } + } + /** * At 07:00 (Server Time) every Monday in every month from June through December. */ From d268eec04acbbcc13ddd5051b70f135ec960e804 Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Tue, 27 Aug 2024 11:08:09 +0300 Subject: [PATCH 33/57] feat: [438] Remove 2y logic for generating reports --- .../services/organization.service.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/backend/src/modules/organization/services/organization.service.ts b/backend/src/modules/organization/services/organization.service.ts index c05aaaf23..4595a2738 100644 --- a/backend/src/modules/organization/services/organization.service.ts +++ b/backend/src/modules/organization/services/organization.service.ts @@ -198,15 +198,8 @@ export class OrganizationService { where: { id: In(createOrganizationDto.activity.domains) }, }); - // BR: ANAF data is available after 30 June each year (aprox). - // If the current date is gt 30 June we query the last year, otherwise 2 years ago const lastYear = new Date().getFullYear() - 1; - const _30June = new Date(new Date().getFullYear(), 5, 30); - const yearToGetAnafData = isBefore(new Date(), _30June) - ? lastYear - 1 - : lastYear; - - // BR: ANAF data will be available only if the organization existed last year, otherwise is futile to try to fetch it + // BR: ANAF data will be available only if the organization existed last year, otherwise is futile to try to fetch it or generate reports. const organizationExistedLastYear = createOrganizationDto.general.yearCreated < new Date().getFullYear(); @@ -214,7 +207,7 @@ export class OrganizationService { const financialInformation = organizationExistedLastYear ? await this.organizationFinancialService.getFinancialInformationFromANAF( createOrganizationDto.general.cui, - yearToGetAnafData, + lastYear, ) : null; @@ -243,7 +236,7 @@ export class OrganizationService { ? { organizationFinancial: this.organizationFinancialService.generateFinancialReportsData( - yearToGetAnafData, + lastYear, financialInformation, ), } @@ -251,9 +244,9 @@ export class OrganizationService { ...(organizationExistedLastYear ? { organizationReport: { - reports: [{ year: yearToGetAnafData }], - partners: [{ year: yearToGetAnafData }], - investors: [{ year: yearToGetAnafData }], + reports: [{ year: lastYear }], + partners: [{ year: lastYear }], + investors: [{ year: lastYear }], }, } : {}), From 51cd9550dbeeb0e095c89fbed85e5c4d30fba7b9 Mon Sep 17 00:00:00 2001 From: Dragos-Paul Strat Date: Tue, 27 Aug 2024 11:23:39 +0300 Subject: [PATCH 34/57] fix: [611] fix selected option for controlled inputs --- frontend/src/components/Select/Select.tsx | 42 ++++++++++++----------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/Select/Select.tsx b/frontend/src/components/Select/Select.tsx index 080960709..eefa29d77 100644 --- a/frontend/src/components/Select/Select.tsx +++ b/frontend/src/components/Select/Select.tsx @@ -77,31 +77,33 @@ const Select = (props: { value={item} itemID={`${props.config.id}__select-${item.name}`} > - {({ selected, active }) => ( - <> - - {props.config.displayedAttribute - ? item[props.config.displayedAttribute] - : item} - - - {selected ? ( + { + ({ selected, active }) => ( + <> - - ) : null} - - )} + + {(props?.selected?.id === item.id) ? ( + + + ) : null} + + ) + } ))} From a66bbaf78df78a237810445a0be7c7b376836fe8 Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Tue, 27 Aug 2024 14:04:01 +0300 Subject: [PATCH 35/57] feat: [438] New API to retrieve errored reports and FE integration --- .../organization-profile.controller.ts | 28 +++++++++++++++ .../src/assets/locales/ro/translation.json | 10 +++--- frontend/src/components/Header/Header.tsx | 26 +++++++++----- .../src/pages/organization/Organization.tsx | 11 ++++-- .../OrganizationData/OrganizationData.tsx | 29 +++++++++++---- .../OrganizationFinancial.tsx | 6 ++++ .../interfaces/OrganizationContext.ts | 19 +++++++++- .../organization/Organization.queries.ts | 36 +++++++++++++++---- .../organization/Organization.service.ts | 5 +++ .../organization-reports-status.interface.ts | 4 +++ 10 files changed, 145 insertions(+), 29 deletions(-) create mode 100644 frontend/src/services/organization/interfaces/organization-reports-status.interface.ts diff --git a/backend/src/modules/organization/controllers/organization-profile.controller.ts b/backend/src/modules/organization/controllers/organization-profile.controller.ts index b279368c5..9f8721a29 100644 --- a/backend/src/modules/organization/controllers/organization-profile.controller.ts +++ b/backend/src/modules/organization/controllers/organization-profile.controller.ts @@ -31,6 +31,8 @@ import { UpdateOrganizationDto } from '../dto/update-organization.dto'; import { Organization } from '../entities'; import { OrganizationRequestService } from '../services/organization-request.service'; import { OrganizationService } from '../services/organization.service'; +import { OrganizationFinancialService } from '../services'; +import { OrganizationReportService } from '../services/organization-report.service'; @ApiTooManyRequestsResponse() @UseInterceptors(ClassSerializerInterceptor) @@ -40,6 +42,8 @@ export class OrganizationProfileController { constructor( private readonly organizationService: OrganizationService, private readonly organizationRequestService: OrganizationRequestService, + private readonly organizationFinancialService: OrganizationFinancialService, + private readonly organizationReportService: OrganizationReportService, ) {} @Roles(Role.ADMIN, Role.EMPLOYEE) @@ -48,6 +52,30 @@ export class OrganizationProfileController { return this.organizationService.findWithRelations(user.organizationId); } + @Roles(Role.ADMIN) + @Get('reports-status') + async getReportsStatus( + @ExtractUser() user: User, + ): Promise<{ + numberOfErroredFinancialReports: number; + numberOfErroredReportsInvestorsPartners: number; + }> { + const numberOfErroredFinancialReports = + await this.organizationFinancialService.countNotCompletedReports( + user.organizationId, + ); + + const numberOfErroredReportsInvestorsPartners = + await this.organizationReportService.countNotCompletedReports( + user.organizationId, + ); + + return { + numberOfErroredFinancialReports, + numberOfErroredReportsInvestorsPartners, + }; + } + @Roles(Role.ADMIN) @ApiBody({ type: UpdateOrganizationDto }) @ApiConsumes('multipart/form-data') diff --git a/frontend/src/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json index 50de52832..c243d8aef 100644 --- a/frontend/src/assets/locales/ro/translation.json +++ b/frontend/src/assets/locales/ro/translation.json @@ -1317,8 +1317,6 @@ "active_users": "Utilizatori în organizație", "handle_users": "Gestionează utilizatori", "view_data": "Vizualizează informațiile", - "financial_reports_are_uptodate": "Rapoartele financiare sunt actualizate", - "financial_reports_are_outdated": "Rapoartele financiare nu sunt actualizate", "error": "Eroare la preluarea datelor", "active_organizations": "Active", "inactive_organizations": "Inactive", @@ -1336,11 +1334,13 @@ }, "financial_reports": { "updated": "Informațiile financiare sunt actualizate", - "not_updated": "Informațiile financiare nu sunt actualizate" + "not_updated": "Informațiile financiare nu sunt actualizate.", + "please_update": "Actualizeaza-ți datele pentru a putea continua sa folosesti NGO Hub." }, "organization_reports": { "updated": "Informațiile din secțiunea “ONG-ul în numere” sunt actualizate", - "not_updated": "Informațiile din secțiunea “ONG-ul în numere” nu sunt actualizate" + "not_updated": "Informațiile din secțiunea “ONG-ul în numere” nu sunt actualizate.", + "please_update": "Adauga noile raportari pentru a putea continua sa folosesti NHO Hub." } }, "activity_title": "Organizație activă în NGO Hub din", @@ -1683,4 +1683,4 @@ "date_added": "Data acordării feedback-ului" } } -} \ No newline at end of file +} diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx index d5eb1a24a..f79e338fc 100644 --- a/frontend/src/components/Header/Header.tsx +++ b/frontend/src/components/Header/Header.tsx @@ -15,6 +15,8 @@ import { useUser } from '../../store/selectors'; import { useTranslation } from 'react-i18next'; import { signInWithRedirect } from 'aws-amplify/auth'; import WarningBanner from '../warning-banner/WarningBanner'; +import { UserRole } from '../../pages/users/enums/UserRole.enum'; +import { useOrganizationReportsStatus } from '../../services/organization/Organization.queries'; interface HeaderProps { openSlidingMenu?: any; @@ -26,7 +28,9 @@ const Header = ({ openSlidingMenu, hideLogInButton }: HeaderProps) => { const navigate = useNavigate(); const { profile } = useUser(); - const { t } = useTranslation('header'); + const { data: reportsStatus } = useOrganizationReportsStatus(profile?.role === UserRole.ADMIN); + + const { t } = useTranslation(['header', 'dashboard']); return ( <> @@ -134,14 +138,18 @@ const Header = ({ openSlidingMenu, hideLogInButton }: HeaderProps) => { )}
    - - + {reportsStatus && reportsStatus.numberOfErroredFinancialReports > 0 && ( + + )} + {reportsStatus && reportsStatus.numberOfErroredReportsInvestorsPartners > 0 && ( + + )} ); diff --git a/frontend/src/pages/organization/Organization.tsx b/frontend/src/pages/organization/Organization.tsx index e0634dc28..4d4d3564b 100644 --- a/frontend/src/pages/organization/Organization.tsx +++ b/frontend/src/pages/organization/Organization.tsx @@ -3,8 +3,12 @@ import React, { useEffect, useState } from 'react'; import { Outlet, useLocation, useNavigate, useParams } from 'react-router-dom'; import { classNames } from '../../common/helpers/tailwind.helper'; import { useErrorToast } from '../../common/hooks/useToast'; -import { useCountiesQuery, useIssuersQuery } from '../../services/nomenclature/Nomenclature.queries'; import { + useCountiesQuery, + useIssuersQuery, +} from '../../services/nomenclature/Nomenclature.queries'; +import { + OrganizationPayload, useOrganizationMutation, useOrganizationQuery, } from '../../services/organization/Organization.queries'; @@ -14,6 +18,7 @@ import { useTranslation } from 'react-i18next'; import ContentWrapper from '../../components/content-wrapper/ContentWrapper'; import { useSelectedOrganization } from '../../store/selectors'; import Select from '../../components/Select/Select'; +import { OrganizationContext } from './interfaces/OrganizationContext'; const Organization = () => { const navigate = useNavigate(); @@ -110,7 +115,9 @@ const Organization = () => { />
    - + ); }; diff --git a/frontend/src/pages/organization/components/OrganizationData/OrganizationData.tsx b/frontend/src/pages/organization/components/OrganizationData/OrganizationData.tsx index c0264dd76..55595b04f 100644 --- a/frontend/src/pages/organization/components/OrganizationData/OrganizationData.tsx +++ b/frontend/src/pages/organization/components/OrganizationData/OrganizationData.tsx @@ -1,4 +1,9 @@ -import { ArrowDownTrayIcon, PencilIcon, TrashIcon, ArrowUpTrayIcon } from '@heroicons/react/24/outline'; +import { + ArrowDownTrayIcon, + PencilIcon, + TrashIcon, + ArrowUpTrayIcon, +} from '@heroicons/react/24/outline'; import React, { useContext, useEffect, useState } from 'react'; import { TableColumn } from 'react-data-table-component'; import { useTranslation } from 'react-i18next'; @@ -37,9 +42,11 @@ import ReportSummaryModal from './components/ReportSummaryModal'; import { InvestorsTableHeaders } from './table-headers/InvestorTable.headers'; import { PartnerTableHeaders } from './table-headers/PartnerTable.headers'; import { ReportsTableHeaders } from './table-headers/ReportsTable.headers'; +import { useQueryClient } from 'react-query'; const OrganizationData = () => { const { id } = useParams(); + const queryClient = useQueryClient(); const [isActivitySummaryModalOpen, setIsActivitySummaryModalOpen] = useState(false); const [selectedReport, setSelectedReport] = useState(null); @@ -317,6 +324,9 @@ const OrganizationData = () => { }, }, { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['organization-reports-status'] }); + }, onError: () => { useErrorToast(t('load_error')); }, @@ -325,13 +335,20 @@ const OrganizationData = () => { }; const onDeleteReport = (row: Report) => { - updateReport({ - organization: { - report: { - reportId: row.id, + updateReport( + { + organization: { + report: { + reportId: row.id, + }, }, }, - }); + { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['organization-reports-status'] }); + }, + }, + ); }; const onDownloadFile = async (row: Partner | Investor) => { diff --git a/frontend/src/pages/organization/components/OrganizationFinancial/OrganizationFinancial.tsx b/frontend/src/pages/organization/components/OrganizationFinancial/OrganizationFinancial.tsx index 491803345..4a6bef739 100644 --- a/frontend/src/pages/organization/components/OrganizationFinancial/OrganizationFinancial.tsx +++ b/frontend/src/pages/organization/components/OrganizationFinancial/OrganizationFinancial.tsx @@ -21,8 +21,11 @@ import { OrganizationFinancialTableHeaders } from './OrganizationFinancialTableH import { OrganizationStatus } from '../../enums/OrganizationStatus.enum'; import LoadingContent from '../../../../components/data-table/LoadingContent'; import { useRetryAnafFinancialMutation } from '../../../../services/organization/Organization.queries'; +import { useQueryClient } from 'react-query'; const OrganizationFinancial = () => { + const queryClient = useQueryClient(); + const [isExpenseReportModalOpen, setIsExpenseReportModalOpen] = useState(false); const [isIncomeReportModalOpen, setIsIncomeReportModalOpen] = useState(false); const [selectedReport, setSelectedReport] = useState(null); @@ -120,6 +123,9 @@ const OrganizationFinancial = () => { }, }, { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['organization-reports-status'] }); + }, onError: () => { useErrorToast(t('save_error', { ns: 'organization' })); }, diff --git a/frontend/src/pages/organization/interfaces/OrganizationContext.ts b/frontend/src/pages/organization/interfaces/OrganizationContext.ts index c2e776870..9f916772e 100644 --- a/frontend/src/pages/organization/interfaces/OrganizationContext.ts +++ b/frontend/src/pages/organization/interfaces/OrganizationContext.ts @@ -1,5 +1,22 @@ +import { UseMutateFunction } from 'react-query'; +import { OrganizationPayload } from '../../../services/organization/Organization.queries'; +import { IOrganizationActivity } from './OrganizationActivity.interface'; +import { IOrganizationFinancial } from './OrganizationFinancial.interface'; +import { IOrganizationGeneral } from './OrganizationGeneral.interface'; +import { IOrganizationLegal } from './OrganizationLegal.interface'; +import { IOrganizationReport } from './OrganizationReport.interface'; + export interface OrganizationContext { disabled: boolean; isLoading: boolean; - updateOrganization: any; + updateOrganization: UseMutateFunction< + | IOrganizationGeneral + | IOrganizationActivity + | IOrganizationFinancial + | IOrganizationLegal + | IOrganizationReport, + unknown, + OrganizationPayload, + unknown + >; } diff --git a/frontend/src/services/organization/Organization.queries.ts b/frontend/src/services/organization/Organization.queries.ts index 79c56ec76..b67471818 100644 --- a/frontend/src/services/organization/Organization.queries.ts +++ b/frontend/src/services/organization/Organization.queries.ts @@ -1,4 +1,4 @@ -import { useMutation, useQuery } from 'react-query'; +import { useMutation, useQuery, useQueryClient } from 'react-query'; import { OrderDirection } from '../../common/enums/sort-direction.enum'; import { PaginatedEntity } from '../../common/interfaces/paginated-entity.interface'; import { Person } from '../../common/interfaces/person.interface'; @@ -31,6 +31,7 @@ import { getOrganizationApplicationRequests, getOrganizationApplications, getOrganizationByProfile, + getOrganizationReportsStatus, getOrganizations, patchOrganization, patchOrganizationByProfile, @@ -43,7 +44,7 @@ import { uploadPartnersByProfile, } from './Organization.service'; -interface OrganizationPayload { +export interface OrganizationPayload { id?: number; organization: { general?: IOrganizationGeneral; @@ -286,6 +287,13 @@ export const useOrganizationByProfileQuery = () => { }); }; +// Used to display errored Reports banners only for Organization Admins +export const useOrganizationReportsStatus = (isAdmin: boolean) => { + return useQuery(['organization-reports-status'], () => getOrganizationReportsStatus(), { + enabled: isAdmin, + }); +}; + export const useOrganizationByProfileMutation = () => { const { setOrganizationGeneral, @@ -345,39 +353,55 @@ export const useOrganizationByProfileMutation = () => { export const useUploadPartnersListByProfile = () => { const { setOrganizationReport } = useStore(); + const queryClient = useQueryClient(); return useMutation( ({ partnerId, data }: { partnerId: number; data: FormData }) => uploadPartnersByProfile(partnerId, data), { - onSuccess: (data: IOrganizationReport) => setOrganizationReport(data), + onSuccess: (data: IOrganizationReport) => { + queryClient.invalidateQueries({ queryKey: ['organization-reports-status'] }); + setOrganizationReport(data); + }, }, ); }; export const useUploadInvestorsByProfileList = () => { const { setOrganizationReport } = useStore(); + const queryClient = useQueryClient(); return useMutation( ({ investorId, data }: { investorId: number; data: FormData }) => uploadInvestorsByProfile(investorId, data), { - onSuccess: (data: IOrganizationReport) => setOrganizationReport(data), + onSuccess: (data: IOrganizationReport) => { + queryClient.invalidateQueries({ queryKey: ['organization-reports-status'] }); + setOrganizationReport(data); + }, }, ); }; export const useDeletePartnerByProfileMutation = () => { + const queryClient = useQueryClient(); const { setOrganizationReport } = useStore(); return useMutation(({ partnerId }: { partnerId: number }) => deletePartnersByProfile(partnerId), { - onSuccess: (data: IOrganizationReport) => setOrganizationReport(data), + onSuccess: (data: IOrganizationReport) => { + queryClient.invalidateQueries({ queryKey: ['organization-reports-status'] }); + setOrganizationReport(data); + }, }); }; export const useDeleteInvestorByProfileMutation = () => { + const queryClient = useQueryClient(); const { setOrganizationReport } = useStore(); return useMutation( ({ investorId }: { investorId: number }) => deleteInvestorsByProfile(investorId), { - onSuccess: (data: IOrganizationReport) => setOrganizationReport(data), + onSuccess: (data: IOrganizationReport) => { + queryClient.invalidateQueries({ queryKey: ['organization-reports-status'] }); + setOrganizationReport(data); + }, }, ); }; diff --git a/frontend/src/services/organization/Organization.service.ts b/frontend/src/services/organization/Organization.service.ts index 690054c20..55165da40 100644 --- a/frontend/src/services/organization/Organization.service.ts +++ b/frontend/src/services/organization/Organization.service.ts @@ -17,12 +17,17 @@ import { mapOrganizationLegalToFormData, } from './OrganizationFormDataMapper.service'; import { IOrganizationFinancial } from '../../pages/organization/interfaces/OrganizationFinancial.interface'; +import { OrganizationReportsStatusAPI } from './interfaces/organization-reports-status.interface'; /**EMPLOYEE && ADMIN */ export const getOrganizationByProfile = (): Promise => { return API.get(`/organization-profile`).then((res) => res.data); }; +export const getOrganizationReportsStatus = (): Promise => { + return API.get(`/organization-profile/reports-status`).then((res) => res.data); +}; + export const patchOrganizationByProfile = ( update: any, logo?: File | null, diff --git a/frontend/src/services/organization/interfaces/organization-reports-status.interface.ts b/frontend/src/services/organization/interfaces/organization-reports-status.interface.ts new file mode 100644 index 000000000..cc7c3bb8d --- /dev/null +++ b/frontend/src/services/organization/interfaces/organization-reports-status.interface.ts @@ -0,0 +1,4 @@ +export interface OrganizationReportsStatusAPI { + numberOfErroredFinancialReports: number; + numberOfErroredReportsInvestorsPartners: number; +} From 31b6a7be6d3689fcd8135068804f1c07e1050576 Mon Sep 17 00:00:00 2001 From: luciatugui Date: Tue, 27 Aug 2024 15:38:45 +0300 Subject: [PATCH 36/57] fix: actionLink on banner & superAdmin app dashboard cards --- frontend/src/components/Header/Header.tsx | 2 ++ .../warning-banner/WarningBanner.tsx | 19 +++++++------ .../DashboardStatistics.constants.ts | 2 +- .../Overview/OrganizationOverview.tsx | 27 ++++++++++--------- 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx index f79e338fc..9df68634a 100644 --- a/frontend/src/components/Header/Header.tsx +++ b/frontend/src/components/Header/Header.tsx @@ -142,12 +142,14 @@ const Header = ({ openSlidingMenu, hideLogInButton }: HeaderProps) => { )} {reportsStatus && reportsStatus.numberOfErroredReportsInvestorsPartners > 0 && ( )} diff --git a/frontend/src/components/warning-banner/WarningBanner.tsx b/frontend/src/components/warning-banner/WarningBanner.tsx index 39664a024..728d71f9d 100644 --- a/frontend/src/components/warning-banner/WarningBanner.tsx +++ b/frontend/src/components/warning-banner/WarningBanner.tsx @@ -3,21 +3,24 @@ import { ExclamationTriangleIcon } from '@heroicons/react/24/solid'; interface WarningBannerProps { text: string; - actionText: string; + actionText?: string; + actionLink?: string; } -const WarningBanner = ({ text, actionText }: WarningBannerProps) => { +const WarningBanner = ({ text, actionText, actionLink }: WarningBannerProps) => { return (

    {text}{' '} - - {actionText} - + {actionText && ( + + {actionText} + + )}

    ); diff --git a/frontend/src/pages/dashboard/constants/DashboardStatistics.constants.ts b/frontend/src/pages/dashboard/constants/DashboardStatistics.constants.ts index 0d701a888..6b2a254f4 100644 --- a/frontend/src/pages/dashboard/constants/DashboardStatistics.constants.ts +++ b/frontend/src/pages/dashboard/constants/DashboardStatistics.constants.ts @@ -160,7 +160,7 @@ export const SuperAdminOverviewExtendedStatisticsMapping = { }, ], button: { - href: `organization/${organizationId}/data`, + href: `/organizations/${organizationId}/data`, label: 'statistics.view_data', }, }), diff --git a/frontend/src/pages/organization/components/Overview/OrganizationOverview.tsx b/frontend/src/pages/organization/components/Overview/OrganizationOverview.tsx index 47d99be29..591177438 100644 --- a/frontend/src/pages/organization/components/Overview/OrganizationOverview.tsx +++ b/frontend/src/pages/organization/components/Overview/OrganizationOverview.tsx @@ -31,18 +31,21 @@ const OrganizationOverview = () => {
    - - +
    + + +
    +
    Date: Tue, 27 Aug 2024 15:43:44 +0300 Subject: [PATCH 37/57] feat: [438] Add testing APIs --- .../controllers/testing.controller.ts | 44 +++++++++++++++++++ .../organization/organization.module.ts | 8 +++- .../organization/services/crons.service.ts | 1 - 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 backend/src/modules/organization/controllers/testing.controller.ts diff --git a/backend/src/modules/organization/controllers/testing.controller.ts b/backend/src/modules/organization/controllers/testing.controller.ts new file mode 100644 index 000000000..59617c2b3 --- /dev/null +++ b/backend/src/modules/organization/controllers/testing.controller.ts @@ -0,0 +1,44 @@ +import { + ClassSerializerInterceptor, + Controller, + Post, + UseInterceptors, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiTooManyRequestsResponse } from '@nestjs/swagger'; +import { Roles } from 'src/common/decorators/roles.decorator'; +import { Role } from 'src/modules/user/enums/role.enum'; +import { OrganizationCronsService } from '../services/crons.service'; + +@ApiTooManyRequestsResponse() +@UseInterceptors(ClassSerializerInterceptor) +@ApiBearerAuth() +@Controller('testing') +export class TestingController { + constructor( + private readonly organizationCronsService: OrganizationCronsService, + ) {} + + @Roles(Role.SUPER_ADMIN) + @Post('generate-reports-1st-january') + generateReports(): Promise { + return this.organizationCronsService.generateFinancialDataAndReportsForPreviousYear(); + } + + @Roles(Role.SUPER_ADMIN) + @Post('fetch-anaf-data') + fetchANAFDataForFinancialReports(): Promise { + return this.organizationCronsService.fetchANAFDataForFinancialReports(); + } + + @Roles(Role.SUPER_ADMIN) + @Post('complete-reports-1st-june') + sendEmailToRemindOrganizationProfileUpdate(): Promise { + return this.organizationCronsService.sendEmailToRemindOrganizationProfileUpdate(); + } + + @Roles(Role.SUPER_ADMIN) + @Post('complete-reports-1st-june-to-30th-june') + sendReminderForOrganizationProfileUpdate(): Promise { + return this.organizationCronsService.sendReminderForOrganizationProfileUpdate(); + } +} diff --git a/backend/src/modules/organization/organization.module.ts b/backend/src/modules/organization/organization.module.ts index 6f5e11fe9..0998dd589 100644 --- a/backend/src/modules/organization/organization.module.ts +++ b/backend/src/modules/organization/organization.module.ts @@ -46,6 +46,7 @@ import { OrganizationRequestHistory } from './entities/organization-request-hist import { PracticeProgramModule } from '../practice-program/practice-program.module'; import { CivicCenterModule } from '../civic-center-service/civic-center.module'; import { OrganizationCronsService } from './services/crons.service'; +import { TestingController } from './controllers/testing.controller'; @Module({ imports: [ @@ -69,7 +70,11 @@ import { OrganizationCronsService } from './services/crons.service'; PracticeProgramModule, CivicCenterModule, ], - controllers: [OrganizationController, OrganizationProfileController], + controllers: [ + OrganizationController, + OrganizationProfileController, + TestingController, + ], providers: [ ContactService, OrganizationService, @@ -98,6 +103,7 @@ import { OrganizationCronsService } from './services/crons.service'; OrganizationRequestService, OrganizationFinancialService, OrganizationReportService, + OrganizationCronsService, // FOR TESTING CONTROLLER PURPOSE ], }) export class OrganizationModule {} diff --git a/backend/src/modules/organization/services/crons.service.ts b/backend/src/modules/organization/services/crons.service.ts index caa5b9c6e..85687dd90 100644 --- a/backend/src/modules/organization/services/crons.service.ts +++ b/backend/src/modules/organization/services/crons.service.ts @@ -15,7 +15,6 @@ import { CompletionStatus, OrganizationFinancialReportStatus, } from '../enums/organization-financial-completion.enum'; -import { InjectEntityManager } from '@nestjs/typeorm'; @Injectable() export class OrganizationCronsService { From ccc6c33a5052376a24143083cf65fff0ab66f579 Mon Sep 17 00:00:00 2001 From: luciatugui Date: Tue, 27 Aug 2024 15:53:04 +0300 Subject: [PATCH 38/57] fix: zIndex on tooltip --- frontend/src/components/status-badge/StatusBadge.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/status-badge/StatusBadge.tsx b/frontend/src/components/status-badge/StatusBadge.tsx index 9f4812787..f33e663b2 100644 --- a/frontend/src/components/status-badge/StatusBadge.tsx +++ b/frontend/src/components/status-badge/StatusBadge.tsx @@ -51,6 +51,7 @@ const StatusBadge = ({ status, value, tooltip, tooltipContent = '' }: StatusBadg content={tooltipContent} style={{ maxWidth: '250px', + zIndex: 100_000, backgroundColor: status === BadgeStatus.SUCCESS ? colors.green[300] From 31befdd1d65bea12a9232e043af9156e35ef32fe Mon Sep 17 00:00:00 2001 From: Dragos-Paul Strat Date: Wed, 28 Aug 2024 15:32:25 +0300 Subject: [PATCH 39/57] fix: [611] remove semibold option + add id selection optional --- frontend/src/components/Select/Select.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Select/Select.tsx b/frontend/src/components/Select/Select.tsx index eefa29d77..6024b2db7 100644 --- a/frontend/src/components/Select/Select.tsx +++ b/frontend/src/components/Select/Select.tsx @@ -82,7 +82,7 @@ const Select = (props: { <> @@ -91,7 +91,7 @@ const Select = (props: { : item} - {(props?.selected?.id === item.id) ? ( + {(props.selected?.id ? props?.selected?.id === item.id : selected) ? ( Date: Thu, 29 Aug 2024 15:02:54 +0300 Subject: [PATCH 40/57] fix: [344] make user phone not mandatory --- .../1724932441086-UserPhoneNullable.ts | 23 +++++++++++++++++++ .../src/modules/user/dto/create-user.dto.ts | 3 +-- .../user/entities/user-history.entity.ts | 2 +- .../src/modules/user/entities/user.entity.ts | 2 +- .../modules/user/services/cognito.service.ts | 10 +++----- .../src/modules/user/services/user.service.ts | 5 +++- .../src/assets/locales/ro/translation.json | 2 +- .../components/UserCreate/UserCreateConfig.ts | 7 +++--- 8 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 backend/src/migrations/1724932441086-UserPhoneNullable.ts diff --git a/backend/src/migrations/1724932441086-UserPhoneNullable.ts b/backend/src/migrations/1724932441086-UserPhoneNullable.ts new file mode 100644 index 000000000..e0065ddad --- /dev/null +++ b/backend/src/migrations/1724932441086-UserPhoneNullable.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UserPhoneNullable1724932441086 implements MigrationInterface { + name = 'UserPhoneNullable1724932441086'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user" ALTER COLUMN "phone" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "user_history" ALTER COLUMN "phone" DROP NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user" ALTER COLUMN "phone" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "user_history" ALTER COLUMN "phone" SET NOT NULL`, + ); + } +} diff --git a/backend/src/modules/user/dto/create-user.dto.ts b/backend/src/modules/user/dto/create-user.dto.ts index 7e9ff9b42..5bad6b254 100644 --- a/backend/src/modules/user/dto/create-user.dto.ts +++ b/backend/src/modules/user/dto/create-user.dto.ts @@ -27,8 +27,7 @@ export class CreateUserDto { @MaxLength(50) email: string; - @IsString() - @IsNotEmpty() + @IsOptional() @IsPhoneValid() phone: string; diff --git a/backend/src/modules/user/entities/user-history.entity.ts b/backend/src/modules/user/entities/user-history.entity.ts index ae8b3b548..ec4da2b0b 100644 --- a/backend/src/modules/user/entities/user-history.entity.ts +++ b/backend/src/modules/user/entities/user-history.entity.ts @@ -35,7 +35,7 @@ export class UserHistory extends BaseEntity implements HistoryEntityInterface { @Column({ type: 'varchar', name: 'email' }) email: string; - @Column({ type: 'varchar', name: 'phone' }) + @Column({ type: 'varchar', name: 'phone', nullable: true }) phone: string; @Column({ type: 'enum', enum: Role, name: 'role', default: Role.EMPLOYEE }) diff --git a/backend/src/modules/user/entities/user.entity.ts b/backend/src/modules/user/entities/user.entity.ts index 2fd584e9e..83cf171f0 100644 --- a/backend/src/modules/user/entities/user.entity.ts +++ b/backend/src/modules/user/entities/user.entity.ts @@ -15,7 +15,7 @@ export class User extends BaseEntity { @Column({ type: 'varchar', name: 'email', unique: true }) email: string; - @Column({ type: 'varchar', name: 'phone' }) + @Column({ type: 'varchar', name: 'phone', nullable: true }) phone: string; @Column({ type: 'enum', enum: Role, name: 'role', default: Role.EMPLOYEE }) diff --git a/backend/src/modules/user/services/cognito.service.ts b/backend/src/modules/user/services/cognito.service.ts index e5429659c..34b644add 100644 --- a/backend/src/modules/user/services/cognito.service.ts +++ b/backend/src/modules/user/services/cognito.service.ts @@ -29,10 +29,7 @@ export class CognitoUserService { Username: email, DesiredDeliveryMediums: [DeliveryMediumType.EMAIL], UserAttributes: [ - { - Name: 'phone_number', - Value: phone, - }, + ...(phone ? [{ Name: 'phone_number', Value: phone }] : []), { Name: 'name', Value: name, @@ -48,9 +45,8 @@ export class CognitoUserService { ], }); - const data: AdminCreateUserCommandOutput = await this.cognitoProvider.send( - createUserCommand, - ); + const data: AdminCreateUserCommandOutput = + await this.cognitoProvider.send(createUserCommand); return data.User.Username; } diff --git a/backend/src/modules/user/services/user.service.ts b/backend/src/modules/user/services/user.service.ts index 1e7a6b941..4b603032b 100644 --- a/backend/src/modules/user/services/user.service.ts +++ b/backend/src/modules/user/services/user.service.ts @@ -372,7 +372,10 @@ export class UserService { ) { throw new BadRequestException(USER_ERRORS.ALREADY_EXISTS_EMAIL); } else if ( - await this.userRepository.get({ where: { phone: createUserDto.phone } }) + createUserDto.phone && + (await this.userRepository.get({ + where: { phone: createUserDto.phone }, + })) ) { throw new BadRequestException(USER_ERRORS.ALREADY_EXISTS_PHONE); } diff --git a/frontend/src/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json index 735b1320a..6007f48c9 100644 --- a/frontend/src/assets/locales/ro/translation.json +++ b/frontend/src/assets/locales/ro/translation.json @@ -953,7 +953,7 @@ "max": "Numărul de telefon poate avea maxim 15 caractere", "min": "Numărul de telefon poate avea minim 10 caractere", "invalid": "Numărul de telefon are un format invalid", - "label": "Telefon*" + "label": "Telefon" } } }, diff --git a/frontend/src/pages/users/components/UserCreate/UserCreateConfig.ts b/frontend/src/pages/users/components/UserCreate/UserCreateConfig.ts index 354348dce..c918253f9 100644 --- a/frontend/src/pages/users/components/UserCreate/UserCreateConfig.ts +++ b/frontend/src/pages/users/components/UserCreate/UserCreateConfig.ts @@ -79,10 +79,6 @@ export const UserCreateConfig: Record = { phone: { key: 'phone', rules: { - required: { - value: true, - message: translations.phone.required, - }, maxLength: { value: 15, message: translations.phone.max, @@ -92,6 +88,9 @@ export const UserCreateConfig: Record = { message: translations.phone.min, }, validate: (value: string) => { + if (!value) { + return true; + } return isValidPhoneNumber(value) || translations.phone.invalid; }, }, From 67b4a8337bfc0be7d4305b366f2a18785f75913c Mon Sep 17 00:00:00 2001 From: Dragos-Paul Strat Date: Thu, 29 Aug 2024 15:57:48 +0300 Subject: [PATCH 41/57] fix: [438] prevent errors while attempting to delete undefined reports --- .../services/organization.service.ts | 80 +++++++++++-------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/backend/src/modules/organization/services/organization.service.ts b/backend/src/modules/organization/services/organization.service.ts index 4595a2738..f6b762f8e 100644 --- a/backend/src/modules/organization/services/organization.service.ts +++ b/backend/src/modules/organization/services/organization.service.ts @@ -58,7 +58,7 @@ import { OrganizationReportService } from './organization-report.service'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { EVENTS } from 'src/modules/notifications/constants/events.contants'; import RestrictOngEvent from 'src/modules/notifications/events/restrict-ong-event.class'; -import { isBefore } from 'date-fns'; +import * as Sentry from '@sentry/node'; @Injectable() export class OrganizationService { @@ -1304,45 +1304,59 @@ export class OrganizationService { await queryRunner.manager.delete(Organization, organizationId); // 2. delete report - const reportIds = organization.organizationReport.reports.map( - (report) => report.id, - ); - await queryRunner.manager.delete(Report, reportIds); + if (organization.organizationReport?.reports.length) { + const reportIds = organization.organizationReport.reports.map( + (report) => report.id, + ); + await queryRunner.manager.delete(Report, reportIds); + } - const inverstorIds = organization.organizationReport.investors.map( - (investor) => investor.id, - ); - await queryRunner.manager.delete(Investor, inverstorIds); + if (organization.organizationReport?.partners.length) { + const inverstorIds = organization.organizationReport.investors.map( + (investor) => investor.id, + ); + await queryRunner.manager.delete(Investor, inverstorIds); + } - const partnerIds = organization.organizationReport.partners.map( - (partner) => partner.id, - ); - await queryRunner.manager.delete(Partner, partnerIds); + if (organization.organizationReport?.partners.length) { + const partnerIds = organization.organizationReport.partners.map( + (partner) => partner.id, + ); + await queryRunner.manager.delete(Partner, partnerIds); + } - await queryRunner.manager.delete( - OrganizationReport, - organization.organizationReportId, - ); + if (organization.organizationReportId) { + await queryRunner.manager.delete( + OrganizationReport, + organization.organizationReportId, + ); + } // 3. delete financial - const organizationFinancialIds = organization.organizationFinancial.map( - (financial) => financial.id, - ); - await queryRunner.manager.delete( - OrganizationFinancial, - organizationFinancialIds, - ); + if (organization.organizationFinancial.length) { + const organizationFinancialIds = organization.organizationFinancial.map( + (financial) => financial.id, + ); + await queryRunner.manager.delete( + OrganizationFinancial, + organizationFinancialIds, + ); + } // 4. delete delete legal - await queryRunner.manager.delete( - Contact, - organization.organizationLegal.legalReprezentativeId, - ); + if (organization.organizationLegal.legalReprezentativeId) { + await queryRunner.manager.delete( + Contact, + organization.organizationLegal.legalReprezentativeId, + ); + } - const directorsIds = organization.organizationLegal.directors.map( - (director) => director.id, - ); - await queryRunner.manager.delete(Contact, directorsIds); + if (organization.organizationLegal.directors.length) { + const directorsIds = organization.organizationLegal.directors.map( + (director) => director.id, + ); + await queryRunner.manager.delete(Contact, directorsIds); + } await queryRunner.manager.delete( OrganizationLegal, @@ -1366,6 +1380,8 @@ export class OrganizationService { // since we have errors lets rollback the changes we made await queryRunner.rollbackTransaction(); + Sentry.captureException(error); + this.logger.error({ error: { error }, ...ORGANIZATION_ERRORS.DELETE.ONG, From fede9005928cd15e2ae5a83a010c64e0166ef49b Mon Sep 17 00:00:00 2001 From: Dragos-Paul Strat Date: Thu, 29 Aug 2024 16:51:38 +0300 Subject: [PATCH 42/57] fix: [438] prevent generation of reports for organizations that have the yearCreated the current year --- .../src/modules/organization/services/crons.service.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/src/modules/organization/services/crons.service.ts b/backend/src/modules/organization/services/crons.service.ts index 85687dd90..6c5e5c606 100644 --- a/backend/src/modules/organization/services/crons.service.ts +++ b/backend/src/modules/organization/services/crons.service.ts @@ -41,10 +41,16 @@ export class OrganizationCronsService { @Cron('0 5 1 1 *') // 1st of January, 5:00 AM async generateFinancialDataAndReportsForPreviousYear() { - const lastYear = new Date().getFullYear() - 1; + const thisYear = new Date().getFullYear(); + const lastYear = thisYear - 1; // 1. Get all organizations with are missing the previous year the financial data and reports const organizations = await this.organizationRepository.getMany({ + where: { + organizationGeneral: { + yearCreated: Not(thisYear), + }, + }, relations: { organizationFinancial: true, organizationGeneral: true, From 80d006deaf03bf543866048da587d01575070cd8 Mon Sep 17 00:00:00 2001 From: Dragos-Paul Strat Date: Thu, 29 Aug 2024 17:43:46 +0300 Subject: [PATCH 43/57] fix: [438] add correct if --- .../src/modules/organization/services/organization.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/modules/organization/services/organization.service.ts b/backend/src/modules/organization/services/organization.service.ts index f6b762f8e..676850ea8 100644 --- a/backend/src/modules/organization/services/organization.service.ts +++ b/backend/src/modules/organization/services/organization.service.ts @@ -1311,7 +1311,7 @@ export class OrganizationService { await queryRunner.manager.delete(Report, reportIds); } - if (organization.organizationReport?.partners.length) { + if (organization.organizationReport?.investors.length) { const inverstorIds = organization.organizationReport.investors.map( (investor) => investor.id, ); From e52284c41f7bd1accd786845ea41b09a182ee88f Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Mon, 2 Sep 2024 15:13:25 +0300 Subject: [PATCH 44/57] bugfix: Handle status and last update for inexistent reports for an organization --- .../1725278083496-OrganizationViewUpdates.ts | 135 ++++++++++++++++++ .../entities/organization-view.entity.ts | 18 ++- .../services/organization.service.ts | 9 +- .../organization-statistics.interface.ts | 2 +- .../statistics/services/statistics.service.ts | 1 + frontend/src/pages/dashboard/Dashboard.tsx | 4 +- .../DashboardStatistics.constants.ts | 14 +- .../Overview/OrganizationOverview.tsx | 4 +- .../OrganizationStatistics.interface.ts | 2 +- .../OrganizationsTable.headers.tsx | 3 +- 10 files changed, 172 insertions(+), 20 deletions(-) create mode 100644 backend/src/migrations/1725278083496-OrganizationViewUpdates.ts diff --git a/backend/src/migrations/1725278083496-OrganizationViewUpdates.ts b/backend/src/migrations/1725278083496-OrganizationViewUpdates.ts new file mode 100644 index 000000000..dcebe0bf3 --- /dev/null +++ b/backend/src/migrations/1725278083496-OrganizationViewUpdates.ts @@ -0,0 +1,135 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class OrganizationViewUpdates1725278083496 + implements MigrationInterface +{ + name = 'OrganizationViewUpdates1725278083496'; + + public async up(queryRunner: QueryRunner): Promise { + // Drop the existing view and its metadata + await queryRunner.query( + `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'OrganizationView', 'public'], + ); + await queryRunner.query(`DROP VIEW "OrganizationView"`); + + // Create the updated view with the following changes: + // 1. Modified the CASE statement for completionStatus: + // - Changed from "OR status IS NULL" to "AND status IS NOT NULL" + // This ensures that only non-null statuses that are not 'Completed' are considered as 'Not Completed' + // 2. Updated the updatedOn column: + // - Added a CASE statement to return NULL if the max date is '1970-01-01' + // - Cast the result to text + await queryRunner.query(`CREATE VIEW "OrganizationView" AS + SELECT + "organization".id AS "id", + "organization".status AS "status", + "organization".created_on AS "createdOn", + CASE + WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != 'Completed' AND "organization_financial".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "report".status != 'Completed' AND "report".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "partner".status != 'Completed' AND "partner".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "investor".status != 'Completed' AND "investor".status IS NOT NULL THEN 1 END) > 0 + THEN 'Not Completed' + ELSE 'Completed' + END AS "completionStatus", + "organization_general".name AS "name", + "organization_general".alias AS "alias", + "organization_general".email AS "adminEmail", + COUNT(DISTINCT "user".id) AS "userCount", + "organization_general".logo AS "logo", + CASE + WHEN MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01'))) = '1970-01-01' + THEN NULL + ELSE MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01')))::text + END AS "updatedOn" + FROM + "organization" "organization" + LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id + LEFT JOIN "user" "user" ON "user".organization_id = "organization".id + AND "user".deleted_on IS NULL + AND "user".ROLE = 'employee' + AND "user".status != 'pending' + LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId" + LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id + LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId" + LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId" + LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId" + WHERE + "organization".status != 'pending' + GROUP BY + "organization".id, + "organization_general".id + `); + + // Insert metadata for the new view + await queryRunner.query( + `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + 'public', + 'VIEW', + 'OrganizationView', + 'SELECT\n "organization".id AS "id",\n "organization".status AS "status",\n "organization".created_on AS "createdOn",\n \tCASE \n WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != \'Completed\' AND "organization_financial".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "report".status != \'Completed\' AND "report".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "partner".status != \'Completed\' AND "partner".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "investor".status != \'Completed\' AND "investor".status IS NOT NULL THEN 1 END) > 0\n THEN \'Not Completed\'\n ELSE \'Completed\'\n \tEND AS "completionStatus",\n "organization_general".name AS "name",\n "organization_general".alias AS "alias",\n "organization_general".email AS "adminEmail",\n COUNT(DISTINCT "user".id) AS "userCount",\n "organization_general".logo AS "logo",\n CASE\n WHEN MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\'))) = \'1970-01-01\'\n THEN NULL\n ELSE MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\')))::text\n END AS "updatedOn"\n FROM\n "organization" "organization"\n LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id\n LEFT JOIN "user" "user" ON "user".organization_id = "organization".id\n AND "user".deleted_on IS NULL\n AND "user".ROLE = \'employee\'\n AND "user".status != \'pending\'\n LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId"\n LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id\n LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId"\n LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId"\n LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId"\n WHERE\n "organization".status != \'pending\'\n GROUP BY\n "organization".id,\n "organization_general".id', + ], + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // Drop the updated view and its metadata + await queryRunner.query( + `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'OrganizationView', 'public'], + ); + await queryRunner.query(`DROP VIEW "OrganizationView"`); + + // Recreate the original view with the following differences: + // 1. CASE statement for completionStatus uses "OR status IS NULL" instead of "AND status IS NOT NULL" + // 2. updatedOn column doesn't have the CASE statement and doesn't cast to text + await queryRunner.query(`CREATE VIEW "OrganizationView" AS SELECT + "organization".id AS "id", + "organization".status AS "status", + "organization".created_on AS "createdOn", + CASE + WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != 'Completed' OR "organization_financial".status IS NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "report".status != 'Completed' OR "report".status IS NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "partner".status != 'Completed' OR "partner".status IS NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "investor".status != 'Completed' OR "investor".status IS NULL THEN 1 END) > 0 + THEN 'Not Completed' + ELSE 'Completed' + END AS "completionStatus", + "organization_general".name AS "name", + "organization_general".alias AS "alias", + "organization_general".email AS "adminEmail", + COUNT(DISTINCT "user".id) AS "userCount", + "organization_general".logo AS "logo", + MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01'))) AS "updatedOn" + FROM + "organization" "organization" + LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id + LEFT JOIN "user" "user" ON "user".organization_id = "organization".id + AND "user".deleted_on IS NULL + AND "user".ROLE = 'employee' + AND "user".status != 'pending' + LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId" + LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id + LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId" + LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId" + LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId" + WHERE + "organization".status != 'pending' + GROUP BY + "organization".id, + "organization_general".id`); + + // Insert metadata for the original view + await queryRunner.query( + `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + 'public', + 'VIEW', + 'OrganizationView', + 'SELECT\n "organization".id AS "id",\n "organization".status AS "status",\n "organization".created_on AS "createdOn",\n \tCASE \n WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != \'Completed\' OR "organization_financial".status IS NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "report".status != \'Completed\' OR "report".status IS NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "partner".status != \'Completed\' OR "partner".status IS NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "investor".status != \'Completed\' OR "investor".status IS NULL THEN 1 END) > 0\n THEN \'Not Completed\'\n ELSE \'Completed\'\n \tEND AS "completionStatus",\n "organization_general".name AS "name",\n "organization_general".alias AS "alias",\n "organization_general".email AS "adminEmail",\n COUNT(DISTINCT "user".id) AS "userCount",\n "organization_general".logo AS "logo",\n MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\'))) AS "updatedOn"\n FROM\n "organization" "organization"\n LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id\n LEFT JOIN "user" "user" ON "user".organization_id = "organization".id\n AND "user".deleted_on IS NULL\n AND "user".ROLE = \'employee\'\n AND "user".status != \'pending\'\n LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId"\n LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id\n LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId"\n LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId"\n LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId"\n WHERE\n "organization".status != \'pending\'\n GROUP BY\n "organization".id,\n "organization_general".id', + ], + ); + } +} diff --git a/backend/src/modules/organization/entities/organization-view.entity.ts b/backend/src/modules/organization/entities/organization-view.entity.ts index 8f233da9f..f6edf7913 100644 --- a/backend/src/modules/organization/entities/organization-view.entity.ts +++ b/backend/src/modules/organization/entities/organization-view.entity.ts @@ -9,19 +9,23 @@ import { OrganizationStatus } from '../enums/organization-status.enum'; "organization".status AS "status", "organization".created_on AS "createdOn", CASE - WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != 'Completed' OR "organization_financial".status IS NULL THEN 1 END) > 0 - OR COUNT(DISTINCT CASE WHEN "report".status != 'Completed' OR "report".status IS NULL THEN 1 END) > 0 - OR COUNT(DISTINCT CASE WHEN "partner".status != 'Completed' OR "partner".status IS NULL THEN 1 END) > 0 - OR COUNT(DISTINCT CASE WHEN "investor".status != 'Completed' OR "investor".status IS NULL THEN 1 END) > 0 + WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != 'Completed' AND "organization_financial".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "report".status != 'Completed' AND "report".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "partner".status != 'Completed' AND "partner".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "investor".status != 'Completed' AND "investor".status IS NOT NULL THEN 1 END) > 0 THEN 'Not Completed' ELSE 'Completed' END AS "completionStatus", "organization_general".name AS "name", "organization_general".alias AS "alias", "organization_general".email AS "adminEmail", - COUNT("user".id) AS "userCount", + COUNT(DISTINCT "user".id) AS "userCount", "organization_general".logo AS "logo", - MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01'))) AS "updatedOn" + CASE + WHEN MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01'))) = '1970-01-01' + THEN NULL + ELSE MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01')))::text + END AS "updatedOn" FROM "organization" "organization" LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id @@ -52,7 +56,7 @@ export class OrganizationView { createdOn: Date; @ViewColumn() - updatedOn: Date; + updatedOn: Date | null; @ViewColumn() name: string; diff --git a/backend/src/modules/organization/services/organization.service.ts b/backend/src/modules/organization/services/organization.service.ts index 4595a2738..afab10c05 100644 --- a/backend/src/modules/organization/services/organization.service.ts +++ b/backend/src/modules/organization/services/organization.service.ts @@ -58,7 +58,7 @@ import { OrganizationReportService } from './organization-report.service'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { EVENTS } from 'src/modules/notifications/constants/events.contants'; import RestrictOngEvent from 'src/modules/notifications/events/restrict-ong-event.class'; -import { isBefore } from 'date-fns'; +import * as Sentry from '@sentry/node'; @Injectable() export class OrganizationService { @@ -1365,9 +1365,10 @@ export class OrganizationService { } catch (error) { // since we have errors lets rollback the changes we made await queryRunner.rollbackTransaction(); - + console.log(error); + Sentry.captureException(error); this.logger.error({ - error: { error }, + error: JSON.stringify(error), ...ORGANIZATION_ERRORS.DELETE.ONG, }); const err = error?.response; @@ -1615,7 +1616,7 @@ export class OrganizationService { }, }); - return data.updatedOn; + return data?.updatedOn; } private flattenOrganization( diff --git a/backend/src/modules/statistics/interfaces/organization-statistics.interface.ts b/backend/src/modules/statistics/interfaces/organization-statistics.interface.ts index bbfcd2c56..0a3eeb804 100644 --- a/backend/src/modules/statistics/interfaces/organization-statistics.interface.ts +++ b/backend/src/modules/statistics/interfaces/organization-statistics.interface.ts @@ -9,7 +9,7 @@ export interface IAllOrganizationsStatistics { export interface IOrganizationStatistics { organizationCreatedOn: Date; - organizationSyncedOn: Date; + organizationSyncedOn: Date | null; numberOfInstalledApps: number; numberOfUsers: number; hubStatistics: IGeneralONGHubStatistics; diff --git a/backend/src/modules/statistics/services/statistics.service.ts b/backend/src/modules/statistics/services/statistics.service.ts index d639e4f98..e83e1e6b8 100644 --- a/backend/src/modules/statistics/services/statistics.service.ts +++ b/backend/src/modules/statistics/services/statistics.service.ts @@ -322,6 +322,7 @@ export class StatisticsService { numberOfErroredReportsInvestorsPartners, }; } catch (error) { + console.log(error); this.logger.error({ error: { error }, ...STATISTICS_ERRORS.ORGANIZATION_STATISTICS, diff --git a/frontend/src/pages/dashboard/Dashboard.tsx b/frontend/src/pages/dashboard/Dashboard.tsx index ae5a6185e..fa9fc4307 100644 --- a/frontend/src/pages/dashboard/Dashboard.tsx +++ b/frontend/src/pages/dashboard/Dashboard.tsx @@ -72,7 +72,9 @@ const Dashboard = () => {
    diff --git a/frontend/src/pages/dashboard/constants/DashboardStatistics.constants.ts b/frontend/src/pages/dashboard/constants/DashboardStatistics.constants.ts index 6b2a254f4..ec60f482b 100644 --- a/frontend/src/pages/dashboard/constants/DashboardStatistics.constants.ts +++ b/frontend/src/pages/dashboard/constants/DashboardStatistics.constants.ts @@ -119,14 +119,17 @@ export const AdminEmployeeDashboardExtendedStatisticsMapping = { icon: UserGroupIcon, info: [{ title: value, subtitle: 'statistics.active_users' }], }), - activity: (values: { organizationCreatedOn: Date; organizationSyncedOn: Date }) => ({ + activity: (values: { organizationCreatedOn: Date; organizationSyncedOn: Date | null }) => ({ icon: ClockIcon, info: [ { subtitle: formatDate(values.organizationCreatedOn), title: 'activity_title', }, - { subtitle: formatDate(values.organizationSyncedOn), title: 'last_updated_on' }, + { + subtitle: values.organizationSyncedOn ? formatDate(values.organizationSyncedOn) : '-', + title: 'last_updated_on', + }, ], }), }; @@ -181,14 +184,17 @@ export const SuperAdminOverviewExtendedStatisticsMapping = { icon: UserGroupIcon, info: [{ title: value, subtitle: 'statistics.active_users' }], }), - activity: (values: { organizationCreatedOn: Date; organizationSyncedOn: Date }) => ({ + activity: (values: { organizationCreatedOn: Date; organizationSyncedOn: Date | null }) => ({ icon: ClockIcon, info: [ { subtitle: formatDate(values.organizationCreatedOn), title: 'activity_title', }, - { subtitle: formatDate(values.organizationSyncedOn), title: 'last_updated_on' }, + { + subtitle: values.organizationSyncedOn ? formatDate(values.organizationSyncedOn) : '-', + title: 'last_updated_on', + }, ], }), }; diff --git a/frontend/src/pages/organization/components/Overview/OrganizationOverview.tsx b/frontend/src/pages/organization/components/Overview/OrganizationOverview.tsx index 591177438..163e6f224 100644 --- a/frontend/src/pages/organization/components/Overview/OrganizationOverview.tsx +++ b/frontend/src/pages/organization/components/Overview/OrganizationOverview.tsx @@ -65,7 +65,9 @@ const OrganizationOverview = () => {
    diff --git a/frontend/src/pages/organization/interfaces/OrganizationStatistics.interface.ts b/frontend/src/pages/organization/interfaces/OrganizationStatistics.interface.ts index 8b9ef4272..712d31bc4 100644 --- a/frontend/src/pages/organization/interfaces/OrganizationStatistics.interface.ts +++ b/frontend/src/pages/organization/interfaces/OrganizationStatistics.interface.ts @@ -9,7 +9,7 @@ export interface IAllOrganizationsStatistics { export interface IOrganizationStatistics { organizationCreatedOn: Date; - organizationSyncedOn: Date; + organizationSyncedOn: Date | null; numberOfInstalledApps: number; numberOfUsers: number; hubStatistics: IGeneralONGHubStatistics; diff --git a/frontend/src/pages/organization/table-headers/OrganizationsTable.headers.tsx b/frontend/src/pages/organization/table-headers/OrganizationsTable.headers.tsx index 68357ef4b..35d084aee 100644 --- a/frontend/src/pages/organization/table-headers/OrganizationsTable.headers.tsx +++ b/frontend/src/pages/organization/table-headers/OrganizationsTable.headers.tsx @@ -65,7 +65,8 @@ export const OrganizationsTableHeaders: TableColumn[] = [ name: , sortable: true, minWidth: '12rem', - selector: (row: IOrganizationView) => formatDate(row?.updatedOn as string), + selector: (row: IOrganizationView) => + row?.updatedOn ? formatDate(row?.updatedOn as string) : '-', }, { id: 'completionStatus', From 0b677979e403fabf13ca90a8866420cd69de83bb Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Tue, 3 Sep 2024 11:52:27 +0300 Subject: [PATCH 45/57] feat: [contracts] expose more data to vic to be able to generate templates --- .../interfaces/user-with-organization.interface.ts | 3 +++ .../_publicAPI/services/user-organization.service.ts | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/src/modules/_publicAPI/interfaces/user-with-organization.interface.ts b/backend/src/modules/_publicAPI/interfaces/user-with-organization.interface.ts index 7794df694..4af96de04 100644 --- a/backend/src/modules/_publicAPI/interfaces/user-with-organization.interface.ts +++ b/backend/src/modules/_publicAPI/interfaces/user-with-organization.interface.ts @@ -13,5 +13,8 @@ export interface IUserWithOrganization { activityArea: string; logo: string; description: string; + cui: string; + legalReprezentativeFullName: string; + legalReprezentativeRole: string; }; } diff --git a/backend/src/modules/_publicAPI/services/user-organization.service.ts b/backend/src/modules/_publicAPI/services/user-organization.service.ts index db9534ccf..ebbff119f 100644 --- a/backend/src/modules/_publicAPI/services/user-organization.service.ts +++ b/backend/src/modules/_publicAPI/services/user-organization.service.ts @@ -38,7 +38,7 @@ export class UserOrganizationService { } // check if there is an organization id - this methods also handles organization not found - const { organizationGeneral, organizationActivity } = + const { organizationGeneral, organizationActivity, organizationLegal } = await this.organizationService.findWithRelations(user.organizationId); // parse organization activityArea to string @@ -61,6 +61,11 @@ export class UserOrganizationService { activityArea: activityArea, logo: organizationGeneral.logo, description: organizationGeneral.shortDescription, + cui: organizationGeneral.cui, + legalReprezentativeFullName: + organizationLegal?.legalReprezentative?.fullName || '', + legalReprezentativeRole: + organizationLegal?.legalReprezentative?.role || '', }, }; } catch (error) { From 6d52618a1c9a208df0efa23fbee8ad318c4a435a Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Tue, 3 Sep 2024 14:06:58 +0300 Subject: [PATCH 46/57] fix: [438] Create organization report when the organization is created --- .../services/organization-report.service.ts | 20 +++++++++++++++---- .../services/organization.service.ts | 11 +++++++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/backend/src/modules/organization/services/organization-report.service.ts b/backend/src/modules/organization/services/organization-report.service.ts index e75a33e80..bc9535136 100644 --- a/backend/src/modules/organization/services/organization-report.service.ts +++ b/backend/src/modules/organization/services/organization-report.service.ts @@ -237,16 +237,27 @@ export class OrganizationReportService { organization: Organization; year: number; }): Promise { - const organizationReport = organization.organizationReport; + let organizationReport = organization.organizationReport; + + if (!organizationReport) { + const newOrganizationReport = + await this.organizationReportRepository.save({ + organization: { id: organization.id }, + reports: [], + partners: [], + investors: [], + }); + organizationReport = newOrganizationReport; + } // Check if the given organizationId has already reports for the given year to avoid duplicating them - const hasReport = organizationReport.reports.find( + const hasReport = organizationReport?.reports.find( (report) => report.year === year, ); - const hasPartners = organizationReport.partners.find( + const hasPartners = organizationReport?.partners.find( (partner) => partner.year === year, ); - const hasInvestors = organizationReport.investors.find( + const hasInvestors = organizationReport?.investors.find( (investor) => investor.year === year, ); @@ -272,6 +283,7 @@ export class OrganizationReportService { : {}), }); } catch (err) { + console.log(err); Sentry.captureException(err, { extra: { organization, diff --git a/backend/src/modules/organization/services/organization.service.ts b/backend/src/modules/organization/services/organization.service.ts index f36c94b41..5dfd3bc09 100644 --- a/backend/src/modules/organization/services/organization.service.ts +++ b/backend/src/modules/organization/services/organization.service.ts @@ -241,15 +241,24 @@ export class OrganizationService { ), } : {}), + // Initialize organizationReport based on whether the organization existed last year ...(organizationExistedLastYear ? { + // If the organization existed last year, create initial reports, partners, and investors for that year organizationReport: { reports: [{ year: lastYear }], partners: [{ year: lastYear }], investors: [{ year: lastYear }], }, } - : {}), + : { + // If the organization is new, initialize empty arrays for reports, partners, and investors + organizationReport: { + reports: [], + partners: [], + investors: [], + }, + }), }); // upload logo From 9976650a73eb5fe9f55c9cc98097bf3cdad11132 Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Tue, 3 Sep 2024 14:17:50 +0300 Subject: [PATCH 47/57] fix: [438] - Change adminEmail to contact person in relation with NGO --- .../1725362133806-OrgViewUpdateAdminEmail.ts | 135 ++++++++++++++++++ .../entities/organization-view.entity.ts | 2 +- 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 backend/src/migrations/1725362133806-OrgViewUpdateAdminEmail.ts diff --git a/backend/src/migrations/1725362133806-OrgViewUpdateAdminEmail.ts b/backend/src/migrations/1725362133806-OrgViewUpdateAdminEmail.ts new file mode 100644 index 000000000..f99493cef --- /dev/null +++ b/backend/src/migrations/1725362133806-OrgViewUpdateAdminEmail.ts @@ -0,0 +1,135 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class OrgViewUpdateAdminEmail1725362133806 + implements MigrationInterface +{ + name = 'OrgViewUpdateAdminEmail1725362133806'; + + public async up(queryRunner: QueryRunner): Promise { + // Drop the existing view and its metadata + await queryRunner.query( + `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'OrganizationView', 'public'], + ); + await queryRunner.query(`DROP VIEW "OrganizationView"`); + + // Create the updated view + // Changes: + // - "adminEmail" now uses "organization_general".contact_person->>'email' instead of "organization_general".email because we are interested in "Contact person in relation with ONGHub" + await queryRunner.query(`CREATE VIEW "OrganizationView" AS + SELECT + "organization".id AS "id", + "organization".status AS "status", + "organization".created_on AS "createdOn", + CASE + WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != 'Completed' AND "organization_financial".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "report".status != 'Completed' AND "report".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "partner".status != 'Completed' AND "partner".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "investor".status != 'Completed' AND "investor".status IS NOT NULL THEN 1 END) > 0 + THEN 'Not Completed' + ELSE 'Completed' + END AS "completionStatus", + "organization_general".name AS "name", + "organization_general".alias AS "alias", + "organization_general".contact_person->>'email' AS "adminEmail", + COUNT(DISTINCT "user".id) AS "userCount", + "organization_general".logo AS "logo", + CASE + WHEN MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01'))) = '1970-01-01' + THEN NULL + ELSE MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01')))::text + END AS "updatedOn" + FROM + "organization" "organization" + LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id + LEFT JOIN "user" "user" ON "user".organization_id = "organization".id + AND "user".deleted_on IS NULL + AND "user".ROLE = 'employee' + AND "user".status != 'pending' + LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId" + LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id + LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId" + LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId" + LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId" + WHERE + "organization".status != 'pending' + GROUP BY + "organization".id, + "organization_general".id + `); + + // Insert metadata for the new view + await queryRunner.query( + `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + 'public', + 'VIEW', + 'OrganizationView', + 'SELECT\n "organization".id AS "id",\n "organization".status AS "status",\n "organization".created_on AS "createdOn",\n \tCASE \n WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != \'Completed\' AND "organization_financial".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "report".status != \'Completed\' AND "report".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "partner".status != \'Completed\' AND "partner".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "investor".status != \'Completed\' AND "investor".status IS NOT NULL THEN 1 END) > 0\n THEN \'Not Completed\'\n ELSE \'Completed\'\n \tEND AS "completionStatus",\n "organization_general".name AS "name",\n "organization_general".alias AS "alias",\n "organization_general".contact_person->>\'email\' AS "adminEmail",\n COUNT(DISTINCT "user".id) AS "userCount",\n "organization_general".logo AS "logo",\n CASE\n WHEN MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\'))) = \'1970-01-01\'\n THEN NULL\n ELSE MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\')))::text\n END AS "updatedOn"\n FROM\n "organization" "organization"\n LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id\n LEFT JOIN "user" "user" ON "user".organization_id = "organization".id\n AND "user".deleted_on IS NULL\n AND "user".ROLE = \'employee\'\n AND "user".status != \'pending\'\n LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId"\n LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id\n LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId"\n LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId"\n LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId"\n WHERE\n "organization".status != \'pending\'\n GROUP BY\n "organization".id,\n "organization_general".id', + ], + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // Drop the updated view and its metadata + await queryRunner.query( + `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'OrganizationView', 'public'], + ); + await queryRunner.query(`DROP VIEW "OrganizationView"`); + + // Recreate the original view + // Changes: + // - "adminEmail" now uses "organization_general".email instead of "organization_general".contact_person->>'email' + await queryRunner.query(`CREATE VIEW "OrganizationView" AS SELECT + "organization".id AS "id", + "organization".status AS "status", + "organization".created_on AS "createdOn", + CASE + WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != 'Completed' AND "organization_financial".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "report".status != 'Completed' AND "report".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "partner".status != 'Completed' AND "partner".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "investor".status != 'Completed' AND "investor".status IS NOT NULL THEN 1 END) > 0 + THEN 'Not Completed' + ELSE 'Completed' + END AS "completionStatus", + "organization_general".name AS "name", + "organization_general".alias AS "alias", + "organization_general".email AS "adminEmail", + COUNT(DISTINCT "user".id) AS "userCount", + "organization_general".logo AS "logo", + CASE + WHEN MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01'))) = '1970-01-01' + THEN NULL + ELSE MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01')))::text + END AS "updatedOn" + FROM + "organization" "organization" + LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id + LEFT JOIN "user" "user" ON "user".organization_id = "organization".id + AND "user".deleted_on IS NULL + AND "user".ROLE = 'employee' + AND "user".status != 'pending' + LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId" + LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id + LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId" + LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId" + LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId" + WHERE + "organization".status != 'pending' + GROUP BY + "organization".id, + "organization_general".id`); + + // Insert metadata for the original view + await queryRunner.query( + `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + 'public', + 'VIEW', + 'OrganizationView', + 'SELECT\n "organization".id AS "id",\n "organization".status AS "status",\n "organization".created_on AS "createdOn",\n \tCASE \n WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != \'Completed\' AND "organization_financial".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "report".status != \'Completed\' AND "report".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "partner".status != \'Completed\' AND "partner".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "investor".status != \'Completed\' AND "investor".status IS NOT NULL THEN 1 END) > 0\n THEN \'Not Completed\'\n ELSE \'Completed\'\n \tEND AS "completionStatus",\n "organization_general".name AS "name",\n "organization_general".alias AS "alias",\n "organization_general".email AS "adminEmail",\n COUNT(DISTINCT "user".id) AS "userCount",\n "organization_general".logo AS "logo",\n CASE\n WHEN MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\'))) = \'1970-01-01\'\n THEN NULL\n ELSE MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\')))::text\n END AS "updatedOn"\n FROM\n "organization" "organization"\n LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id\n LEFT JOIN "user" "user" ON "user".organization_id = "organization".id\n AND "user".deleted_on IS NULL\n AND "user".ROLE = \'employee\'\n AND "user".status != \'pending\'\n LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId"\n LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id\n LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId"\n LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId"\n LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId"\n WHERE\n "organization".status != \'pending\'\n GROUP BY\n "organization".id,\n "organization_general".id', + ], + ); + } +} diff --git a/backend/src/modules/organization/entities/organization-view.entity.ts b/backend/src/modules/organization/entities/organization-view.entity.ts index f6edf7913..043246322 100644 --- a/backend/src/modules/organization/entities/organization-view.entity.ts +++ b/backend/src/modules/organization/entities/organization-view.entity.ts @@ -18,7 +18,7 @@ import { OrganizationStatus } from '../enums/organization-status.enum'; END AS "completionStatus", "organization_general".name AS "name", "organization_general".alias AS "alias", - "organization_general".email AS "adminEmail", + "organization_general".contact_person->>'email' AS "adminEmail", COUNT(DISTINCT "user".id) AS "userCount", "organization_general".logo AS "logo", CASE From 3c6cc688cfe734e761ea51e0d33c2bc935d6d694 Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Tue, 3 Sep 2024 14:57:38 +0300 Subject: [PATCH 48/57] fix: [438] Throw error if data received from ANAF is missing the NGO indicators --- .../organization-financial.service.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/backend/src/modules/organization/services/organization-financial.service.ts b/backend/src/modules/organization/services/organization-financial.service.ts index 57696a9b9..a6bee3e47 100644 --- a/backend/src/modules/organization/services/organization-financial.service.ts +++ b/backend/src/modules/organization/services/organization-financial.service.ts @@ -184,6 +184,35 @@ export class OrganizationFinancialService { return obj.indicator === 'I46'; }); + // If any of the required indicators are undefined, it likely means + // the CUI is not for an NGO or the ANAF service structure has changed + if ( + income === undefined || + expense === undefined || + employees === undefined + ) { + const missingIndicators = [ + income === undefined ? 'I38 (income)' : null, + expense === undefined ? 'I40 (expense)' : null, + employees === undefined ? 'I46 (employees)' : null, + ] + .filter(Boolean) + .join(', '); + + const sentryMessage = `ANAF data missing required indicators (${missingIndicators}) for CUI: ${cui}, Year: ${year}`; + Sentry.captureMessage(sentryMessage, { + level: 'warning', + extra: { + cui, + year, + anafData, + missingIndicators, + }, + }); + this.logger.warn(sentryMessage); + return null; + } + return { numberOfEmployees: employees?.val_indicator, totalExpense: expense?.val_indicator, From 4d6d1ecbf4bdc7dfd89f1e59fe295a163036c134 Mon Sep 17 00:00:00 2001 From: Manda Andrei <49789938+andrei-manda@users.noreply.github.com> Date: Tue, 3 Sep 2024 15:42:12 +0300 Subject: [PATCH 49/57] fix: [438] change email title & body --- backend/src/mail/constants/template.constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/mail/constants/template.constants.ts b/backend/src/mail/constants/template.constants.ts index 748b3dfb2..291b85e3a 100644 --- a/backend/src/mail/constants/template.constants.ts +++ b/backend/src/mail/constants/template.constants.ts @@ -137,7 +137,7 @@ export const MAIL_OPTIONS: Record = { WEEKLY_REMINDER_TO_UPDATE_ORGANIZATION_REPORTS: { template: ORGANIZATION_REQUEST, subject: - 'Actualizați profilul organizației până pe 30 iunie pentru a evita suspendarea contului', + 'Reminder: Actualizați profilul organizației până pe 30 iunie pentru a evita suspendarea contului', context: { title: 'Actualizați profilul organizației până pe 30 iunie pentru a evita suspendarea contului', @@ -188,7 +188,7 @@ export const MAIL_OPTIONS: Record = {
  • Salvează modificările
  • -

    Este important să faci aceste actualizări înainte de 30 iunie 2024 pentru a evita suspendarea contului tău NGO Hub.

    +

    Este important să faci aceste actualizări pentru a evita suspendarea contului tău NGO Hub.

    Dacă ai nevoie de ajutor sau ai orice fel de întrebare, ne poți contacta oricând la civic@code4.ro sau poți programa o sesiune Civic Tech 911 direct din contul NGO Hub al organizației.

    `, From 2382eaac529280550150722b93aae73d34bfdecf Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Tue, 3 Sep 2024 15:57:56 +0300 Subject: [PATCH 50/57] fix: [438] Include financial information 'Pending' status in the organization completionStatus calculation --- ...161763-OrgViewChangeFinancialStatusCalc.ts | 121 ++++++++++++++++++ .../entities/organization-view.entity.ts | 2 +- 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 backend/src/migrations/1725368161763-OrgViewChangeFinancialStatusCalc.ts diff --git a/backend/src/migrations/1725368161763-OrgViewChangeFinancialStatusCalc.ts b/backend/src/migrations/1725368161763-OrgViewChangeFinancialStatusCalc.ts new file mode 100644 index 000000000..59b46561c --- /dev/null +++ b/backend/src/migrations/1725368161763-OrgViewChangeFinancialStatusCalc.ts @@ -0,0 +1,121 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class OrgViewChangeFinancialStatusCalc1725368161763 + implements MigrationInterface +{ + name = 'OrgViewChangeFinancialStatusCalc1725368161763'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'OrganizationView', 'public'], + ); + await queryRunner.query(`DROP VIEW "OrganizationView"`); + await queryRunner.query(`CREATE VIEW "OrganizationView" AS + SELECT + "organization".id AS "id", + "organization".status AS "status", + "organization".created_on AS "createdOn", + CASE + WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status NOT IN ('Completed', 'Pending') AND "organization_financial".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "report".status != 'Completed' AND "report".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "partner".status != 'Completed' AND "partner".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "investor".status != 'Completed' AND "investor".status IS NOT NULL THEN 1 END) > 0 + THEN 'Not Completed' + ELSE 'Completed' + END AS "completionStatus", + "organization_general".name AS "name", + "organization_general".alias AS "alias", + "organization_general".contact_person->>'email' AS "adminEmail", + COUNT(DISTINCT "user".id) AS "userCount", + "organization_general".logo AS "logo", + CASE + WHEN MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01'))) = '1970-01-01' + THEN NULL + ELSE MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01')))::text + END AS "updatedOn" + FROM + "organization" "organization" + LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id + LEFT JOIN "user" "user" ON "user".organization_id = "organization".id + AND "user".deleted_on IS NULL + AND "user".ROLE = 'employee' + AND "user".status != 'pending' + LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId" + LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id + LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId" + LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId" + LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId" + WHERE + "organization".status != 'pending' + GROUP BY + "organization".id, + "organization_general".id + `); + await queryRunner.query( + `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + 'public', + 'VIEW', + 'OrganizationView', + 'SELECT\n "organization".id AS "id",\n "organization".status AS "status",\n "organization".created_on AS "createdOn",\n \tCASE \n WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status NOT IN (\'Completed\', \'Pending\') AND "organization_financial".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "report".status != \'Completed\' AND "report".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "partner".status != \'Completed\' AND "partner".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "investor".status != \'Completed\' AND "investor".status IS NOT NULL THEN 1 END) > 0\n THEN \'Not Completed\'\n ELSE \'Completed\'\n \tEND AS "completionStatus",\n "organization_general".name AS "name",\n "organization_general".alias AS "alias",\n "organization_general".contact_person->>\'email\' AS "adminEmail",\n COUNT(DISTINCT "user".id) AS "userCount",\n "organization_general".logo AS "logo",\n CASE\n WHEN MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\'))) = \'1970-01-01\'\n THEN NULL\n ELSE MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\')))::text\n END AS "updatedOn"\n FROM\n "organization" "organization"\n LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id\n LEFT JOIN "user" "user" ON "user".organization_id = "organization".id\n AND "user".deleted_on IS NULL\n AND "user".ROLE = \'employee\'\n AND "user".status != \'pending\'\n LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId"\n LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id\n LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId"\n LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId"\n LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId"\n WHERE\n "organization".status != \'pending\'\n GROUP BY\n "organization".id,\n "organization_general".id', + ], + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'OrganizationView', 'public'], + ); + await queryRunner.query(`DROP VIEW "OrganizationView"`); + await queryRunner.query(`CREATE VIEW "OrganizationView" AS SELECT + "organization".id AS "id", + "organization".status AS "status", + "organization".created_on AS "createdOn", + CASE + WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != 'Completed' AND "organization_financial".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "report".status != 'Completed' AND "report".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "partner".status != 'Completed' AND "partner".status IS NOT NULL THEN 1 END) > 0 + OR COUNT(DISTINCT CASE WHEN "investor".status != 'Completed' AND "investor".status IS NOT NULL THEN 1 END) > 0 + THEN 'Not Completed' + ELSE 'Completed' + END AS "completionStatus", + "organization_general".name AS "name", + "organization_general".alias AS "alias", + "organization_general".contact_person->>'email' AS "adminEmail", + COUNT(DISTINCT "user".id) AS "userCount", + "organization_general".logo AS "logo", + CASE + WHEN MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01'))) = '1970-01-01' + THEN NULL + ELSE MAX(GREATEST(COALESCE("organization_financial".updated_on, '1970-01-01'), COALESCE("report".updated_on, '1970-01-01'), COALESCE("partner".updated_on, '1970-01-01'), COALESCE("investor".updated_on, '1970-01-01')))::text + END AS "updatedOn" + FROM + "organization" "organization" + LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id + LEFT JOIN "user" "user" ON "user".organization_id = "organization".id + AND "user".deleted_on IS NULL + AND "user".ROLE = 'employee' + AND "user".status != 'pending' + LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId" + LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id + LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId" + LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId" + LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId" + WHERE + "organization".status != 'pending' + GROUP BY + "organization".id, + "organization_general".id`); + await queryRunner.query( + `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + 'public', + 'VIEW', + 'OrganizationView', + 'SELECT\n "organization".id AS "id",\n "organization".status AS "status",\n "organization".created_on AS "createdOn",\n \tCASE \n WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != \'Completed\' AND "organization_financial".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "report".status != \'Completed\' AND "report".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "partner".status != \'Completed\' AND "partner".status IS NOT NULL THEN 1 END) > 0\n OR COUNT(DISTINCT CASE WHEN "investor".status != \'Completed\' AND "investor".status IS NOT NULL THEN 1 END) > 0\n THEN \'Not Completed\'\n ELSE \'Completed\'\n \tEND AS "completionStatus",\n "organization_general".name AS "name",\n "organization_general".alias AS "alias",\n "organization_general".contact_person->>\'email\' AS "adminEmail",\n COUNT(DISTINCT "user".id) AS "userCount",\n "organization_general".logo AS "logo",\n CASE\n WHEN MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\'))) = \'1970-01-01\'\n THEN NULL\n ELSE MAX(GREATEST(COALESCE("organization_financial".updated_on, \'1970-01-01\'), COALESCE("report".updated_on, \'1970-01-01\'), COALESCE("partner".updated_on, \'1970-01-01\'), COALESCE("investor".updated_on, \'1970-01-01\')))::text\n END AS "updatedOn"\n FROM\n "organization" "organization"\n LEFT JOIN "organization_general" "organization_general" ON "organization".organization_general_id = "organization_general".id\n LEFT JOIN "user" "user" ON "user".organization_id = "organization".id\n AND "user".deleted_on IS NULL\n AND "user".ROLE = \'employee\'\n AND "user".status != \'pending\'\n LEFT JOIN "organization_financial" "organization_financial" ON "organization".id = "organization_financial"."organizationId"\n LEFT JOIN "organization_report" "organization_report" ON "organization".organization_report_id = "organization_report".id\n LEFT JOIN "report" "report" ON "organization_report".id = "report"."organizationReportId"\n LEFT JOIN "partner" "partner" ON "organization_report".id = "partner"."organizationReportId"\n LEFT JOIN "investor" "investor" ON "organization_report".id = "investor"."organizationReportId"\n WHERE\n "organization".status != \'pending\'\n GROUP BY\n "organization".id,\n "organization_general".id', + ], + ); + } +} diff --git a/backend/src/modules/organization/entities/organization-view.entity.ts b/backend/src/modules/organization/entities/organization-view.entity.ts index 043246322..533bdb49c 100644 --- a/backend/src/modules/organization/entities/organization-view.entity.ts +++ b/backend/src/modules/organization/entities/organization-view.entity.ts @@ -9,7 +9,7 @@ import { OrganizationStatus } from '../enums/organization-status.enum'; "organization".status AS "status", "organization".created_on AS "createdOn", CASE - WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status != 'Completed' AND "organization_financial".status IS NOT NULL THEN 1 END) > 0 + WHEN COUNT(DISTINCT CASE WHEN "organization_financial".status NOT IN ('Completed', 'Pending') AND "organization_financial".status IS NOT NULL THEN 1 END) > 0 OR COUNT(DISTINCT CASE WHEN "report".status != 'Completed' AND "report".status IS NOT NULL THEN 1 END) > 0 OR COUNT(DISTINCT CASE WHEN "partner".status != 'Completed' AND "partner".status IS NOT NULL THEN 1 END) > 0 OR COUNT(DISTINCT CASE WHEN "investor".status != 'Completed' AND "investor".status IS NOT NULL THEN 1 END) > 0 From 484a4ece849667747d8442f1d528d7176bff9caf Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Wed, 4 Sep 2024 11:03:04 +0300 Subject: [PATCH 51/57] fix: Downloading NGOs tabel will crash the app because of inefficient query --- .../services/organization.service.ts | 23 ++++++++----------- frontend/src/services/API.tsx | 4 ++-- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/backend/src/modules/organization/services/organization.service.ts b/backend/src/modules/organization/services/organization.service.ts index 5dfd3bc09..c1cf41309 100644 --- a/backend/src/modules/organization/services/organization.service.ts +++ b/backend/src/modules/organization/services/organization.service.ts @@ -373,14 +373,13 @@ export class OrganizationService { 'organizationLegal', 'organizationLegal.legalReprezentative', 'organizationLegal.directors', - 'organizationFinancial', - 'organizationReport', - 'organizationReport.reports', - 'organizationReport.partners', - 'organizationReport.investors', + // 'organizationFinancial', + // 'organizationReport', + // 'organizationReport.reports', + // 'organizationReport.partners', + // 'organizationReport.investors', ], }); - const ignoreKeys = [ 'organizationGeneralId', 'organizationActivityId', @@ -500,11 +499,11 @@ export class OrganizationService { 'organizationLegal.legalReprezentative.phone': 'Legal Representative Phone', 'organizationLegal.directors': 'Directors', - organizationFinancial: 'Organization Financials', - 'organizationReport.id': 'Organization Report ID', - 'organizationReport.reports': 'Reports', - 'organizationReport.partners': 'Partners', - 'organizationReport.investors': 'Investors', + // organizationFinancial: 'Organization Financials', + // 'organizationReport.id': 'Organization Report ID', + // 'organizationReport.reports': 'Reports', + // 'organizationReport.partners': 'Partners', + // 'organizationReport.investors': 'Investors', 'organizationGeneral.contact.name': 'Contact Name', 'organizationGeneral.organizationCity': 'Organization City', 'organizationGeneral.organizationCounty': 'Organization County', @@ -534,11 +533,9 @@ export class OrganizationService { } return res; } - const flatten = organizations.map((org) => { return flattenObject(org); }); - return flatten; } diff --git a/frontend/src/services/API.tsx b/frontend/src/services/API.tsx index 58ad5f251..7e1ebed91 100644 --- a/frontend/src/services/API.tsx +++ b/frontend/src/services/API.tsx @@ -54,14 +54,14 @@ const AxiosInterceptor = ({ children }: AxiosInterceptorProps) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any async (error: any) => { // Redirect to login once we have restricted access - if (error.response.status === 401) { + if (error.response?.status === 401) { await signOut(); // set initial application state setAuthState({ isAuthenticated: false, isRestricted: false, restrictedReason: '' }); } // If use doesn't have access to resource redirect to home - if (error.response.status === 403) { + if (error.response?.status === 403) { // this will trigger the redirect to restricted page setAuthState({ isAuthenticated: true, From 475a81b6488054e193f24db8a2a1a700ad5f8edd Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Wed, 4 Sep 2024 11:27:19 +0300 Subject: [PATCH 52/57] fix: [438] Send financial information reminder emails and fetch anaf data only for ACTIVE organizations --- backend/src/modules/organization/services/crons.service.ts | 3 +++ .../organization/services/organization-financial.service.ts | 2 ++ 2 files changed, 5 insertions(+) diff --git a/backend/src/modules/organization/services/crons.service.ts b/backend/src/modules/organization/services/crons.service.ts index 6c5e5c606..702d0bb4d 100644 --- a/backend/src/modules/organization/services/crons.service.ts +++ b/backend/src/modules/organization/services/crons.service.ts @@ -15,6 +15,7 @@ import { CompletionStatus, OrganizationFinancialReportStatus, } from '../enums/organization-financial-completion.enum'; +import { OrganizationStatus } from '../enums/organization-status.enum'; @Injectable() export class OrganizationCronsService { @@ -108,6 +109,7 @@ export class OrganizationCronsService { const organizations: { adminEmail: string }[] = await this.organizationViewRepository.getMany({ where: { + status: OrganizationStatus.ACTIVE, completionStatus: CompletionStatus.NOT_COMPLETED, }, select: { @@ -151,6 +153,7 @@ export class OrganizationCronsService { const organizations: { adminEmail: string }[] = await this.organizationViewRepository.getMany({ where: { + status: OrganizationStatus.ACTIVE, completionStatus: CompletionStatus.NOT_COMPLETED, }, select: { diff --git a/backend/src/modules/organization/services/organization-financial.service.ts b/backend/src/modules/organization/services/organization-financial.service.ts index a6bee3e47..1d9a808ab 100644 --- a/backend/src/modules/organization/services/organization-financial.service.ts +++ b/backend/src/modules/organization/services/organization-financial.service.ts @@ -22,6 +22,7 @@ import * as Sentry from '@sentry/node'; import { In, Not } from 'typeorm'; import { EVENTS } from 'src/modules/notifications/constants/events.contants'; import InvalidFinancialReportsEvent from 'src/modules/notifications/events/invalid-financial-reports-event.class'; +import { OrganizationStatus } from '../enums/organization-status.enum'; @Injectable() export class OrganizationFinancialService { @@ -295,6 +296,7 @@ export class OrganizationFinancialService { organizationGeneral: true, }, where: { + status: OrganizationStatus.ACTIVE, organizationFinancial: { synched_anaf: false, }, From 3051e8ce426efffaf0614f401eaff69ec9c5e2df Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Wed, 4 Sep 2024 17:19:54 +0300 Subject: [PATCH 53/57] fix: [438] Send "Invalid" financial information from "Refetch ANAF Data CRON" to "Persoana in relatia cu ONG" --- .../organization-financial.service.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/backend/src/modules/organization/services/organization-financial.service.ts b/backend/src/modules/organization/services/organization-financial.service.ts index 1d9a808ab..1136c29cf 100644 --- a/backend/src/modules/organization/services/organization-financial.service.ts +++ b/backend/src/modules/organization/services/organization-financial.service.ts @@ -23,6 +23,7 @@ import { In, Not } from 'typeorm'; import { EVENTS } from 'src/modules/notifications/constants/events.contants'; import InvalidFinancialReportsEvent from 'src/modules/notifications/events/invalid-financial-reports-event.class'; import { OrganizationStatus } from '../enums/organization-status.enum'; +import { ContactPerson } from '../interfaces/contact-person.interface'; @Injectable() export class OrganizationFinancialService { @@ -286,7 +287,11 @@ export class OrganizationFinancialService { type OrganizationsWithMissingANAFData = { id: number; organizationFinancial: OrganizationFinancial[]; - organizationGeneral: { cui: string; email: string }; + organizationGeneral: { + cui: string; + email: string; + contact: ContactPerson; + }; }; const data: OrganizationsWithMissingANAFData[] = @@ -306,6 +311,9 @@ export class OrganizationFinancialService { organizationGeneral: { cui: true, email: true, + contact: { + email: true, + }, }, organizationFinancial: true, }, @@ -398,10 +406,15 @@ export class OrganizationFinancialService { } // In case one of the report is invalid, we notify the ADMIN to modify them - if (sendNotificationForInvalidData) { + if ( + sendNotificationForInvalidData && + org.organizationGeneral?.contact?.email + ) { this.eventEmitter.emit( EVENTS.INVALID_FINANCIAL_REPORTS, - new InvalidFinancialReportsEvent(org.organizationGeneral.email), + new InvalidFinancialReportsEvent( + org.organizationGeneral?.contact?.email, + ), ); } } catch (err) { From 1695b2d024d78f8028562daca5dd541d9c32655a Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Wed, 4 Sep 2024 17:30:58 +0300 Subject: [PATCH 54/57] fix: [438] Information financial in requests was crashing due to missing data --- .../components/OrganizationFinancial/OrganizationFinancial.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/organization/components/OrganizationFinancial/OrganizationFinancial.tsx b/frontend/src/pages/organization/components/OrganizationFinancial/OrganizationFinancial.tsx index 4a6bef739..de87faa24 100644 --- a/frontend/src/pages/organization/components/OrganizationFinancial/OrganizationFinancial.tsx +++ b/frontend/src/pages/organization/components/OrganizationFinancial/OrganizationFinancial.tsx @@ -144,7 +144,7 @@ const OrganizationFinancial = () => {

    {organization?.status === OrganizationStatus.PENDING && - organizationFinancial[0].synched_anaf === false && + organizationFinancial[0]?.synched_anaf === false && role == UserRole.SUPER_ADMIN && (

    From 00e3789efe778ac3bb27d8ea924de90b48c3552d Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Thu, 5 Sep 2024 10:27:18 +0300 Subject: [PATCH 55/57] fix: [438] Set Financial Report as invalid only if the organization completed some of the data --- ...18140608-AllowTotalAndEmployeesToBeNull.ts | 41 +++++++++++++++++++ .../entities/organization-financial.entity.ts | 4 +- .../organization-financial.service.ts | 30 +++++++++++--- .../OrganizationFinancialTableHeaders.tsx | 5 ++- .../components/ExpenseReportModal.tsx | 20 ++++----- .../components/IncomeReportModal.tsx | 18 ++++---- .../OrganizationFinancial.interface.ts | 4 +- .../interfaces/ReportModalProps.interface.ts | 2 +- 8 files changed, 93 insertions(+), 31 deletions(-) create mode 100644 backend/src/migrations/1725518140608-AllowTotalAndEmployeesToBeNull.ts diff --git a/backend/src/migrations/1725518140608-AllowTotalAndEmployeesToBeNull.ts b/backend/src/migrations/1725518140608-AllowTotalAndEmployeesToBeNull.ts new file mode 100644 index 000000000..a71a93d69 --- /dev/null +++ b/backend/src/migrations/1725518140608-AllowTotalAndEmployeesToBeNull.ts @@ -0,0 +1,41 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AllowTotalAndEmployeesToBeNull1725518140608 + implements MigrationInterface +{ + name = 'AllowTotalAndEmployeesToBeNull1725518140608'; + + /** + * Allow Total and Employees to be null to better reflect if the user has not completed the financials or is just the default value + */ + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "organization_financial" ALTER COLUMN "number_of_employees" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "organization_financial" ALTER COLUMN "number_of_employees" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "organization_financial" ALTER COLUMN "total" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "organization_financial" ALTER COLUMN "total" DROP DEFAULT`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "organization_financial" ALTER COLUMN "total" SET DEFAULT '0'`, + ); + await queryRunner.query( + `ALTER TABLE "organization_financial" ALTER COLUMN "total" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "organization_financial" ALTER COLUMN "number_of_employees" SET DEFAULT '0'`, + ); + await queryRunner.query( + `ALTER TABLE "organization_financial" ALTER COLUMN "number_of_employees" SET NOT NULL`, + ); + } +} diff --git a/backend/src/modules/organization/entities/organization-financial.entity.ts b/backend/src/modules/organization/entities/organization-financial.entity.ts index 4b7da50ef..15772d2c4 100644 --- a/backend/src/modules/organization/entities/organization-financial.entity.ts +++ b/backend/src/modules/organization/entities/organization-financial.entity.ts @@ -33,14 +33,14 @@ export class OrganizationFinancial extends BaseEntity { @Column({ type: 'integer', name: 'number_of_employees', - default: 0, + nullable: true, }) numberOfEmployees: number; @Column({ type: 'integer', name: 'year' }) year: number; - @Column({ type: 'integer', name: 'total', default: 0 }) + @Column({ type: 'integer', name: 'total', nullable: true }) total: number; @Column({ diff --git a/backend/src/modules/organization/services/organization-financial.service.ts b/backend/src/modules/organization/services/organization-financial.service.ts index 1136c29cf..8c1e73f52 100644 --- a/backend/src/modules/organization/services/organization-financial.service.ts +++ b/backend/src/modules/organization/services/organization-financial.service.ts @@ -148,21 +148,41 @@ export class OrganizationFinancialService { ]; } + /** + * Determines the status of a financial report based on the organization's input and ANAF data. + * + * @param addedByOrganizationTotal - The total amount entered by the organization (per each category). + * @param anafTotal - The total amount retrieved from ANAF. + * @param isSynced - Whether the report has been synced with ANAF data. + * @returns The status of the financial report. + */ private determineReportStatus( addedByOrganizationTotal: number, anafTotal: number, isSynced: boolean, - ) { + ): OrganizationFinancialReportStatus { + // If the organization hasn't entered any data, the report is not completed + if (addedByOrganizationTotal === null) { + return OrganizationFinancialReportStatus.NOT_COMPLETED; + } + + // If the report is synced with ANAF data if (isSynced) { + // If the totals match, the report is completed if (anafTotal === addedByOrganizationTotal) { return OrganizationFinancialReportStatus.COMPLETED; } else { + // If the totals don't match, the report is invalid return OrganizationFinancialReportStatus.INVALID; } - } else if (addedByOrganizationTotal !== 0) { - return OrganizationFinancialReportStatus.PENDING; } else { - return OrganizationFinancialReportStatus.NOT_COMPLETED; + // If not synced but the organization has entered some data, the report is pending + if (addedByOrganizationTotal !== 0) { + return OrganizationFinancialReportStatus.PENDING; + } else { + // If not synced and no data entered, the report is not completed + return OrganizationFinancialReportStatus.NOT_COMPLETED; + } } } @@ -337,7 +357,7 @@ export class OrganizationFinancialService { const existingData = curr.data as Object; let existingTotal = null; - if (curr.data) { + if (existingData) { existingTotal = Object.keys(existingData).reduce( (acc, curr) => acc + +existingData[curr], 0, diff --git a/frontend/src/pages/organization/components/OrganizationFinancial/OrganizationFinancialTableHeaders.tsx b/frontend/src/pages/organization/components/OrganizationFinancial/OrganizationFinancialTableHeaders.tsx index a2755a98b..286e3fdcc 100644 --- a/frontend/src/pages/organization/components/OrganizationFinancial/OrganizationFinancialTableHeaders.tsx +++ b/frontend/src/pages/organization/components/OrganizationFinancial/OrganizationFinancialTableHeaders.tsx @@ -87,7 +87,7 @@ export const OrganizationFinancialTableHeaders: TableColumn, - selector: (row: IOrganizationFinancial) => row.numberOfEmployees, + selector: (row: IOrganizationFinancial) => row.numberOfEmployees ?? '-', sortable: true, minWidth: '8rem', grow: 0, @@ -95,7 +95,8 @@ export const OrganizationFinancialTableHeaders: TableColumn, - selector: (row: IOrganizationFinancial) => formatCurrency(row?.total), + selector: (row: IOrganizationFinancial) => + row?.total !== null ? formatCurrency(row?.total) : '-', sortable: true, minWidth: '7rem', grow: 0, diff --git a/frontend/src/pages/organization/components/OrganizationFinancial/components/ExpenseReportModal.tsx b/frontend/src/pages/organization/components/OrganizationFinancial/components/ExpenseReportModal.tsx index 54c0b19ad..19733be0e 100644 --- a/frontend/src/pages/organization/components/OrganizationFinancial/components/ExpenseReportModal.tsx +++ b/frontend/src/pages/organization/components/OrganizationFinancial/components/ExpenseReportModal.tsx @@ -17,7 +17,7 @@ import { ANAF_HELPER_LINK } from '../../../../../common/constants/constants'; const ExpenseReportModal = ({ onClose, readonly, - total = 0, + total, year, defaultValue, onSave, @@ -110,9 +110,9 @@ const ExpenseReportModal = ({

    - {`${formatCurrency( - total, - )} RON`} + + {total !== null && total !== undefined ? `${formatCurrency(total)} RON` : 'N/A'} + {t('modal.expense')} @@ -177,16 +177,16 @@ const ExpenseReportModal = ({ {t('modal.defalcat')} - {`${totalDefalcat} `} - {totalDefalcat !== total && ( + {`${totalDefalcat} RON`} + {total !== null && total !== undefined && totalDefalcat !== total && ( {total > totalDefalcat ? `(${formatCurrency(total - totalDefalcat)} RON ${t( - 'modal.unallocated', - )})` + 'modal.unallocated', + )})` : `(${formatCurrency(totalDefalcat - total)} RON ${t( - 'modal.excess', - )})`} + 'modal.excess', + )})`} )} diff --git a/frontend/src/pages/organization/components/OrganizationFinancial/components/IncomeReportModal.tsx b/frontend/src/pages/organization/components/OrganizationFinancial/components/IncomeReportModal.tsx index f039064a7..85bab2232 100644 --- a/frontend/src/pages/organization/components/OrganizationFinancial/components/IncomeReportModal.tsx +++ b/frontend/src/pages/organization/components/OrganizationFinancial/components/IncomeReportModal.tsx @@ -18,7 +18,7 @@ const IncomeReportModal = ({ onClose, readonly, year, - total = 0, + total, defaultValue, onSave, }: ReportModalProps) => { @@ -110,9 +110,9 @@ const IncomeReportModal = ({
    - {`${formatCurrency( - total, - )} RON`} + + {total !== null && total !== undefined ? `${formatCurrency(total)} RON` : 'N/A'} + {t('modal.income')} @@ -178,15 +178,15 @@ const IncomeReportModal = ({ {`${totalDefalcat} `} - {totalDefalcat !== total && ( + {total !== null && total !== undefined && totalDefalcat !== total && ( {total > totalDefalcat ? `(${formatCurrency(total - totalDefalcat)} RON ${t( - 'modal.unallocated', - )})` + 'modal.unallocated', + )})` : `(${formatCurrency(totalDefalcat - total)} RON ${t( - 'modal.excess', - )})`} + 'modal.excess', + )})`} )} diff --git a/frontend/src/pages/organization/interfaces/OrganizationFinancial.interface.ts b/frontend/src/pages/organization/interfaces/OrganizationFinancial.interface.ts index 1307b37f8..199aa342e 100644 --- a/frontend/src/pages/organization/interfaces/OrganizationFinancial.interface.ts +++ b/frontend/src/pages/organization/interfaces/OrganizationFinancial.interface.ts @@ -9,9 +9,9 @@ import { FinancialType } from '../enums/FinancialType.enum'; export interface IOrganizationFinancial extends BaseEntity { type: FinancialType; - numberOfEmployees: number; + numberOfEmployees: number | null; year: number; - total: number; + total: number | null; synched_anaf: boolean; data: Partial | Partial | null; status: CompletionStatus; diff --git a/frontend/src/pages/organization/interfaces/ReportModalProps.interface.ts b/frontend/src/pages/organization/interfaces/ReportModalProps.interface.ts index 8fc1decfd..dc67665d1 100644 --- a/frontend/src/pages/organization/interfaces/ReportModalProps.interface.ts +++ b/frontend/src/pages/organization/interfaces/ReportModalProps.interface.ts @@ -4,7 +4,7 @@ import { Income } from './Income.interface'; export interface ReportModalProps { onClose: () => void; year?: number; - total?: number; + total?: number | null; readonly?: boolean; defaultValue?: Partial | Partial | null; onSave: (data: Partial | Partial) => void; From 263b2b166d7f9a45dc0c3989e5ed8511d9429ca8 Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Thu, 24 Oct 2024 14:18:29 +0300 Subject: [PATCH 56/57] fix: remove sending emails to organizations during fetch ANAF data until December --- .../organization-financial.service.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/backend/src/modules/organization/services/organization-financial.service.ts b/backend/src/modules/organization/services/organization-financial.service.ts index 8c1e73f52..f14786d41 100644 --- a/backend/src/modules/organization/services/organization-financial.service.ts +++ b/backend/src/modules/organization/services/organization-financial.service.ts @@ -426,17 +426,18 @@ export class OrganizationFinancialService { } // In case one of the report is invalid, we notify the ADMIN to modify them - if ( - sendNotificationForInvalidData && - org.organizationGeneral?.contact?.email - ) { - this.eventEmitter.emit( - EVENTS.INVALID_FINANCIAL_REPORTS, - new InvalidFinancialReportsEvent( - org.organizationGeneral?.contact?.email, - ), - ); - } + // ! COMMENTED FOR NOW AS PER MIRUNA REQUEST UNTIL FURTHER NOTICE + // if ( + // sendNotificationForInvalidData && + // org.organizationGeneral?.contact?.email + // ) { + // this.eventEmitter.emit( + // EVENTS.INVALID_FINANCIAL_REPORTS, + // new InvalidFinancialReportsEvent( + // org.organizationGeneral?.contact?.email, + // ), + // ); + // } } catch (err) { Sentry.captureException(err, { extra: { From 64721e9d794069bcdaa7e35f663fad0ae8494dc8 Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Thu, 24 Oct 2024 14:24:58 +0300 Subject: [PATCH 57/57] Production release --- backend/package.json | 2 +- frontend/package.json | 2 +- frontend/src/common/constants/version.constants.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/package.json b/backend/package.json index fe626a9d1..1bcc4a717 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "backend", - "version": "0.0.1", + "version": "1.1.0", "description": "", "author": "", "private": true, diff --git a/frontend/package.json b/frontend/package.json index 72040eed0..8680ae21b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "0.1.0", + "version": "1.1.0", "private": true, "dependencies": { "@headlessui/react": "^2.1.1", diff --git a/frontend/src/common/constants/version.constants.ts b/frontend/src/common/constants/version.constants.ts index 41b62cd67..8b6a6a4e5 100644 --- a/frontend/src/common/constants/version.constants.ts +++ b/frontend/src/common/constants/version.constants.ts @@ -1 +1 @@ -export const FRONTEND_VERSION = 'v0.1.2'; +export const FRONTEND_VERSION = 'v1.1.0';