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/constants/application.constants.ts b/backend/src/modules/application/constants/application.constants.ts index 27d353eec..ed5d0f177 100644 --- a/backend/src/modules/application/constants/application.constants.ts +++ b/backend/src/modules/application/constants/application.constants.ts @@ -10,6 +10,7 @@ export const ORGANIZATION_ALL_APPS_COLUMNS = [ 'ongApp.status as "ongStatus"', 'ongApp.created_on as "createdOn"', 'application.type as type', + 'applicationLabel.name as "applicationLabel"', ]; export const APPLICATIONS_FILES_DIR = 'applications'; 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/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/interfaces/ong-application.interface.ts b/backend/src/modules/application/interfaces/ong-application.interface.ts index 80dcb4b4c..902e88e26 100644 --- a/backend/src/modules/application/interfaces/ong-application.interface.ts +++ b/backend/src/modules/application/interfaces/ong-application.interface.ts @@ -1,3 +1,4 @@ +import { ApplicationLabel } from 'src/shared/entities/application-labels.entity'; import { ApplicationPullingType } from '../enums/application-pulling-type.enum'; import { ApplicationStatus } from '../enums/application-status.enum'; import { ApplicationTypeEnum } from '../enums/ApplicationType.enum'; @@ -23,4 +24,5 @@ export interface IOngApplicationDetails extends IOngApplication { videoLink: string; userStatus: UserOngApplicationStatus; pullingType: ApplicationPullingType; + applicationLabel: ApplicationLabel; } diff --git a/backend/src/modules/application/services/application.service.ts b/backend/src/modules/application/services/application.service.ts index 901e8637f..90d5ef357 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,17 @@ export class ApplicationService { }; } - return this.applicationRepository.update({ id }, applicationPayload); + let applicationLabel = null; + if (applicationPayload.applicationLabel) { + applicationLabel = await this.saveAndGetApplicationLabel( + applicationPayload.applicationLabel, + ); + } + + return this.applicationRepository.update( + { id }, + { ...applicationPayload, applicationLabel }, + ); } catch (error) { this.logger.error({ error: { error }, @@ -502,4 +532,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/modules/application/services/ong-application.service.ts b/backend/src/modules/application/services/ong-application.service.ts index 6dc627bc3..31c101c39 100644 --- a/backend/src/modules/application/services/ong-application.service.ts +++ b/backend/src/modules/application/services/ong-application.service.ts @@ -91,7 +91,7 @@ export class OngApplicationService { : OngApplicationStatus.ACTIVE, }); - if(application.type === ApplicationTypeEnum.STANDALONE) { + if (application.type === ApplicationTypeEnum.STANDALONE) { // 8. trigger emails for admin and super-admin this.eventEmitter.emit( EVENTS.REQUEST_APPLICATION_ACCESS, @@ -99,12 +99,11 @@ export class OngApplicationService { ); } - return ongApp; } catch (error) { Sentry.captureException(error, { - extra: {organizationId, applicationId}, - }) + extra: { organizationId, applicationId }, + }); this.logger.error({ error: { error }, ...ONG_APPLICATION_ERRORS.CREATE }); const err = error?.response; throw new BadRequestException({ @@ -174,6 +173,11 @@ export class OngApplicationService { const applicationsQuery = this.applicationRepository .getQueryBuilder() .select(ORGANIZATION_ALL_APPS_COLUMNS) + .leftJoin( + '_application-label', + 'applicationLabel', + 'applicationLabel.id = application.application_label_id', + ) .leftJoin( 'ong_application', 'ongApp', diff --git a/backend/src/shared/controllers/nomenclatures.controller.ts b/backend/src/shared/controllers/nomenclatures.controller.ts index 26060590d..9135f7927 100644 --- a/backend/src/shared/controllers/nomenclatures.controller.ts +++ b/backend/src/shared/controllers/nomenclatures.controller.ts @@ -13,69 +13,86 @@ import { NomenclaturesService } from '../services'; import { CacheInterceptor } from '@nestjs/cache-manager'; @Public() -@UseInterceptors(ClassSerializerInterceptor, CacheInterceptor) +@UseInterceptors(ClassSerializerInterceptor) @Controller('nomenclatures') export class NomenclaturesController { constructor(private nomenclaturesService: NomenclaturesService) {} @Get('cities') + @UseInterceptors(CacheInterceptor) getCities(@Query() citySearchDto: CitySearchDto) { return this.nomenclaturesService.getCitiesSearch(citySearchDto); } @Get('counties') + @UseInterceptors(CacheInterceptor) getCounties() { return this.nomenclaturesService.getCounties({}); } @Get('domains') + @UseInterceptors(CacheInterceptor) getDomains() { return this.nomenclaturesService.getDomains({}); } @Get('regions') + @UseInterceptors(CacheInterceptor) getRegions() { return this.nomenclaturesService.getRegions({}); } @Get('federations') + @UseInterceptors(CacheInterceptor) getFederations() { return this.nomenclaturesService.getFederations({}); } @Get('coalitions') + @UseInterceptors(CacheInterceptor) getCoalitions() { return this.nomenclaturesService.getCoalitions({}); } @Get('faculties') + @UseInterceptors(CacheInterceptor) getFaculties(@Query() { search }: FacultySearchDto) { const options = search ? { where: { name: ILike(`%${search}%`) } } : {}; return this.nomenclaturesService.getFaculties(options); } @Get('skills') + @UseInterceptors(CacheInterceptor) getSkills() { return this.nomenclaturesService.getSkills({}); } @Get('practice-domains') + @UseInterceptors(CacheInterceptor) getPracticeDomains() { return this.nomenclaturesService.getPracticeDomains({}); } @Get('service-domains') + @UseInterceptors(CacheInterceptor) getServiceDomains() { return this.nomenclaturesService.getServiceDomains({}); } @Get('beneficiaries') + @UseInterceptors(CacheInterceptor) getBeneficiaries() { return this.nomenclaturesService.getBeneficiaries({}); } @Get('issuers') + @UseInterceptors(CacheInterceptor) getIssuers() { return this.nomenclaturesService.getIssuers({}); } + + @Get('application-labels') + getApplicationLabels() { + return this.nomenclaturesService.getApplicationLabels({}); + } } diff --git a/backend/src/shared/entities/application-labels.entity.ts b/backend/src/shared/entities/application-labels.entity.ts new file mode 100644 index 000000000..74b687b83 --- /dev/null +++ b/backend/src/shared/entities/application-labels.entity.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/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/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json index 0e215641a..d25ead90c 100644 --- a/frontend/src/assets/locales/ro/translation.json +++ b/frontend/src/assets/locales/ro/translation.json @@ -27,6 +27,7 @@ "completed": "Completat", "not_completed": "Necompletat" }, + "add_option": "Adaugă opțiunea", "any": "Toate", "restricted": "Restricționat", "active": "Activ", @@ -1062,6 +1063,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", @@ -1097,6 +1107,11 @@ "min": "Pasul trebuie să aibă minim 2 caractere", "max": "Pasul trebuie să aibă maxim 100 de caractere", "label": "Pas" + }, + "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" } }, "request_modal": { 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 && (